Perform a search with MEteor and React


#1

What is the best way (i would love some working example) to let my users to perform a search in a given collection?

I red something about embedding a blaze template that do the job but I prefer to keep everything in react

Thank you so much


#2

Unfortunately on closed source, but my favorite move, as long as the collection is not too huge, is to fetch the collection on the client and to use fuse.js do the heavy work. With it you can achieve fuzzy and facetted search. In case the collection would be too huge for a subscription, you can delegate this with a Meteor.call on the server or simply export your data with a river (plugins for elastic search) to a near shore server with elastic search.

This applies to React but also to any other view layer as well :wink:


#3

I have been using meteorhacks/search-source for more than a year now and it always worked great! :slight_smile:
Just make sure to _ensureIndex on your collections properly! :wink:


#4

SEARCH COMPONENT

//top-level imports
import React from 'react';
import {  StyleSheet, css } from 'aphrodite';
import { browserHistory, Link } from 'react-router';
//antd;
import { Button, Row, Col, Input } from 'antd';



export class SearchArea extends React.Component {
	constructor(props){
		super(props);
		this.state = {
			searchValue: '',
			loading: false
		}
		this.onChange = this.onChange.bind(this);
		this.onSubmit = this.onSubmit.bind(this);
	}
	onChange(e){
		let searchValue = e.target.value;
		this.setState({ searchValue });
	}
	onSubmit(e){
		e.preventDefault();
		this.setState({loading: true});

			browserHistory.push({
				pathname: '/results', 
				query: { name: this.state.searchValue } 
			});
			this.setState({loading: false});

		
	}
	render(){

		return (
				<form onSubmit={this.onSubmit}>
					<Row type="flex" align="middle" justify="center">					
						<Col xs={17} sm={20}>
							<Input value={this.state.searchValue} onChange={this.onChange} />
						</Col>
						<Col xs={7} sm={4}>
							<Button loading={this.state.loading} htmlType="submit">
								{!this.state.loading ? 'SEARCH & COMPARE' : 'SEARCHING...'}
							</Button>
				        </Col>
			      	</Row>
		      	</form>
		);
	}
}

SEARCH RESULTS CONTAINER


import { composeWithTracker } from 'react-komposer';
import { Vendors } from '../../../api/Vendors/Vendors.js';
import { Results } from './Results.js';
import { Loading } from '../common';
import { Meteor } from 'meteor/meteor';

const composer = (props, onData) => {
	
	const searchTerms = props.query;
  const vendorsSub = Meteor.subscribe('Vendors.vendorSearch', searchTerms);
  if (vendorsSub.ready()) {
  	if ( searchTerms ) {
	  		let regex = new RegExp( searchTerms, 'i' );
	  		query = { title: regex };
	  		projection =   { };
	  		const vendors = Vendors.find(query, projection).fetch();
    		onData(null, { vendors });
    		return;
	}
    	const vendors = [];

    	onData(null, { vendors, searchTerms });
  }
};

export default composeWithTracker(composer, Loading)(Results);

PUBLICATION

Meteor.publish('Vendors.vendorSearch', function(search){
	check( search, Match.OneOf( String, null, undefined ) );

	if (!search) {
		return Vendors.find();
	}

		  if ( search ) {
	  		let regex = new RegExp( search, 'i' );
	  		query = { title: regex };
	  		projection =   {  };
		  }
	return Vendors.find(query, projection);
});

#5

For context, the above is using meteor/react with the antd framework and a project that is setup similar to meteor chef’s base repo. It’s meant to search through a collection of “vendors”. You take the search terms from the input and put them into the URL as a query string. Then on the next route, you grab the query string (ie the serach terms) in a komposer container and pass them to a meteor subscription. What you get back are the search results that you will pass to a component (e.g. “Results.js”)… which in my example is the component being wrapped by the komposer container.

This is a free article from TMC: https://themeteorchef.com/tutorials/simple-search

He also has a newer react-based article but it is in the paid section of his website.


#6

Thank you a.com I’ll try this today :slight_smile:


#7

What I did is 2 types of search directly with Mongo/Minimongo - $regex on client and $text Mongo operators through a method on server.

So if a certain collection does not have rich text, but only fields with simple texts, the MongoDB query is formed on client (using $regex, and considering it is subscribed so all the searchable data is already on client). Whereas if there is at least one searchable rich text field in a collection, then a server method is called and the query is formed with $text Mongo operator right on the server.


#8

@pierreeric

Tossing up between fuse.js and Algolia at the moment. Any idea on what fuse.js would consider large?

Thinking about it, I guess the best way to incorporate it would be a method call that returns the collection as JSON - OR - doing the search on the server and returning just the results… The first one has the benefit of not needing multiple calls to the server with each key-stroke with the drawback of needing, possibly, a few seconds onLoad…


#9

It depends too much on your client CPU and your data. You should perform some tries to check if it fits your use cases.


#10

I am having trouble allowing my users to text search a collection. Will someone post an example or shed some guidance please? I have checked out the ok grow article as well the meteor chef, but haven’t been able to connect the dots. I am using meteor and react. I am also very new to both. The biggest issue is getting around mini mongo for the text search. I tried meteor publish/subscribe and my searchValue is null on the server side.

Here is my client side.

import { Meteor } from 'meteor/meteor';
import React from 'react';
import Articles from '../api/articles.js';
import { Session } from 'meteor/session';


export default class Search extends React.Component {

constructor(props){
  super(props);
  this.state = {
    searchValue: ''
  }
}
onChange(e) {
  let searchValue = e.target.value;
  this.setState({searchValue});
}
  onSubmit(e) {
    let searchValue  = this.state.searchValue;
      e.preventDefault();

      if(searchValue){
      Meteor.subscribe("articles", Session.get(searchValue));
  }
}

render() {
  return (
    <div>
    <form onSubmit={this.onSubmit.bind(this)}>
          <input
           type="text"
           name="text"
           placeholder="Search.."
           value={this.state.searchValue}
           onChange={this.onChange.bind(this)}/>
          <button>Search</button>
          </form>

    </div>
  )
}

}

Here is my server.

import { Meteor } from 'meteor/meteor';

export const Articles = new Mongo.Collection('articles');

if( Meteor.isServer) {
    Meteor.publish('articles', function(searchValue) {
      console.log(searchValue);
    Articles.find({$text: {$search: searchValue}});
    });
  }

#11

Well, that should be

Meteor.subscribe("articles", this.state.searchValue);

However, you don’t subscribe to Collections in React like that because your subscriptions might be wiped out upon rerender.

If you put your search result in another unrelated component, you should either use something like Redux or React.Context to wire your search term to this component and subscribe there using withTracker()

Also, better do the check for null/empty values on the server.


#12

Ok, thank you very much. this.state.searchValue worked. I will work on implementing the rest of your suggestions as well.