简单搜索要求多个检索词能够以任意顺序出现在多个字段中,目前的解决方法有两种思路:
一是创建一个额外的检索项,把所有要检索的字段用字符串方式组合起来放在检索项中, 然后用正则表达式匹配; 这种方式实际上有信息冗余,所以原始数据变化后一定要及时更新检索项;
二是用第三方包实现这种功能。
自定义搜索:检索词放在单独Collection中
优点是检索词单独存放,需要去掉或者更新检索词时, 只要简单地drop掉检索词Collection即可,适于存储大量记录;
缺点有两个:
首先显示查询结果比较麻烦,需要先在检索项集合中搜出命中目标的_id
,
然后再查询原始数据表显示完整信息;
其次这种数据结构对组合查询支持不好,比如要查询"名称"字段中含有"abc",
并且"类别"字段中含有"xyz"的展会,由于"类别"字段是一个复杂对象,
需要事先组合成一个字符串,这一项如果放在单独的collection中,
需要查询两个不同的集合,然后取它们的并集。
首先在MongoDB中,创建用于简单搜索的新collection "simpleSearch":
db.fairs.find().forEach(function(elem) {
var searchStr = elem.chnName + elem.engName + elem.position;
db.simpleSearch.save({ _id: elem._id, searchBody: searchStr });
})
然后定义路由处理函数:
Fairs = new Mongo.Collection("fairs");
SimpleSearch = new Mongo.Collection("simpleSearch");
Router.route('/results/:inp', function() {
// basic search url: /results/abc?type=basic
// complex search url: /results/?type=complex?name=xxx?time=xxx?position=xxx?category=xxx
var fullStr = this.params.inp;
var searchType = this.params.query.type;
if (searchType === "basic") {
var parts = fullStr.trim().split(" ");
for (i=0; i<parts.length; ++i) {
parts[i] = "(?=.*" + parts[i] + ")";
}
var queryPtn = new RegExp("(" + parts.join("") + ")", "i");
var ids = [];
SimpleSearch.find( { searchBody: queryPtn }, { fields: { searchBody:0 }} ).forEach(
function(elem) {
ids.push(elem._id);
});
this.render('SearchResults', {
data: function() {
return Fairs.find( { _id: { $in: ids } } );
}
});
} else if (searchType === "complex") {
...
}
});
这里首先查询SimpleSearch集合,将查询得到的展会_id
存入临时容器ids数组中,
然后在Fairs集合中通过"$in"操作符取得所有ids数组中_id
对应的展会对象。
自定义搜索:检索词放在原始信息文档中
这个方法把原始信息和检索项放在同一个集合中,几个检索项放在集合的一个key下面, 集合会比较臃肿,但查询和显示都方便,适用于尺寸不大的数据库。
控制器定义:
Fairs = new Mongo.Collection("fairs");
Router.route('/results/:inp', function() {
// basic search url: /results/abc?type=basic
// complex search url: /results/?type=complex?name=xxx?time=xxx?position=xxx?category=xxx
var fullStr = this.params.inp;
var searchType = this.params.query.type;
if (searchType === "basic") {
var parts = fullStr.trim().split(" ");
for (i=0; i<parts.length; ++i) {
parts[i] = "(?=.*" + parts[i] + ")";
}
var queryPtn = new RegExp("(" + parts.join("") + ")", "i");
this.render('SearchResults', {
data: function() {
return Fairs.find( { "indexStr.simpleSearch": queryPtn } );
}
});
} else if (searchType === "complex") {
...
}
});
基于Search Source的实现方法
这个搜索基于search-source.
通过meteor add meteorhacks:search-source
添加到meteor中,
代码参照了meteor-instant-search-demo.
准备数据源
导入数据
-
下载zips.json; 参考Is there a sample MongoDB Database along the lines of world for MySql?.
-
导入Meteor数据库中:启动meteor服务,然后在另一个shell里运行:
~/apps/mongodb-linux-x86_64-2.6.5/bin/mongoimport -h localhost:3001 --db meteor --collection zips --type json --file zips.json
; -
增加搜索字段:由于简单搜索要求输入可能匹配任意字段, 能达到这个要求的唯一方法就是将被查询的字段连接成一个完整的“搜索”字符串, 输入的各项可以以任何顺序出现在这个搜索字符串(这里这一项名为"forsearch")中, 下面增加这一项:
db.zips.find().forEach(function(elem) { db.zips.update( {_id: elem._id}, { $set: {forsearch: elem.city + ' ' + elem.state}} ); }); db.fairs.find().forEach(function(elem) { var cat_minor = _.reduce(elem.category.minor db.fairs.update( {_id: elem._id}, { $set: {forsearch: elem.chnName + ' ' + elem.engName + ' ' + elem.time + ' ' + elem.position + catStr}} ); });
搜索逻辑
下面"/"都指项目根目录。
数据集定义
在/collections.js中定义数据集和索引:
Zips = new Mongo.Collection('zips');
if(Meteor.isServer) {
Zips._ensureIndex({city: 1, state: 1});
}
服务端
在/server/server.js中定义:
SearchSource.defineSource('zips', function(searchText, options) {
var options = {sort: {isoScore: -1}, limit: 20};
if(searchText) {
var regExp = buildRegExp(searchText);
var selector = {forsearch: regExp};
return Zips.find(selector, options).fetch();
} else {
return Zips.find({}, options).fetch();
}
});
function buildRegExp(searchText) {
var parts = searchText.trim().split(/[ \-\:]+/);
for (i=0; i<parts.length; ++i) {
parts[i] = "(?=.*" + parts[i] + ")";
}
var res = new RegExp("(" + parts.join("") + ")", "i");
return res;
}
buildRegExp函数中的正则表达式(?=.*A)(?=.*B)
可以实现对A和B任意顺序的匹配,
即既能匹配A.B,又能匹配B.A,
参考match string in any word order regex.
客户端:搜索输入框框即时响应
搜索框是通用控件,所以定义在了/client/comTemp.js中,定义了对框中keyup事件的响应:
Template.Header.events({
"keyup #search-input": _.throttle(function(e) {
var text = $(e.target).val().trim();
ZipSearch.search(text);
}, 200)
});
客户端:搜索结果展示页面
定义在/client/results.html中。
<template name="SearchResults">
<div id="search-result" class="container content">
<div id="search-meta">
{{#if isLoading}}
searching ...
{{/if}}
</div>
{{#each getZips}}
<div class="package">
<h4 class="name">
{{{city}}}
</h4>
<div class="description">
{{{state}}}, {{{pop}}}
</div>
</div>
{{/each}}
</div>
</template>
客户端:搜索结果的数据控制
定义在/client/results.js中。
var options = {
keepHistory: 1000 * 60 * 5,
localSearch: true
};
var fields = ['forsearch']; // 这里定义要搜索的字段
ZipSearch = new SearchSource('zips', fields, options);
// 这里的zips要与服务端SearchSource.defineSource中定义的名字一致
Template.SearchResults.helpers({
getZips: function() {
return ZipSearch.getData({
transform: function(matchText, regExp) {
console.log("matchText: " + matchText);
console.log("regExp: " + regExp);
var res = matchText.replace(regExp, "<b>$&</b>");
console.log("res is: " + res);
return res;
},
sort: {isoScore: -1}
});
},
isLoading: function() {
return ZipSearch.getStatus().loading;
}
});
Template.SearchResults.rendered = function() {
ZipSearch.search('');
};