MongoDB 全文和部分文本搜索
MongoDB Full and Partial Text Search
环境:
- MongoDB (3.2.0) 与猫鼬
Collection:
- 用户
创建文本索引:
BasicDBObject keys = new BasicDBObject();
keys.put("name","text");
BasicDBObject options = new BasicDBObject();
options.put("name", "userTextSearch");
options.put("unique", Boolean.FALSE);
options.put("background", Boolean.TRUE);
userCollection.createIndex(keys, options); // using MongoTemplate
文档:
- {"姓名":"莱昂内尔"}
查询:
db.users.find( { "$text" : { "$search" : "LEONEL" } } )
=> 找到
db.users.find( { "$text" : { "$search" : "leonel" } } )
=> FOUND(搜索区分大小写为 false)
db.users.find( { "$text" : { "$search" : "LEONÉL" } } )
=> 已找到(使用 diacriticSensitive 搜索是错误的)
db.users.find( { "$text" : { "$search" : "LEONE" } } )
=> 已找到(部分搜索)
db.users.find( { "$text" : { "$search" : "LEO" } } )
=> 未找到(部分搜索)
db.users.find( { "$text" : { "$search" : "L" } } )
=> 未找到(部分搜索)
知道为什么我使用查询“LEO”或“L”得到 0 个结果吗?
不允许使用带有文本索引搜索的正则表达式。
db.getCollection('users')
.find( { "$text" : { "$search" : "/LEO/i",
"$caseSensitive": false,
"$diacriticSensitive": false }} )
.count() // 0 results
db.getCollection('users')
.find( { "$text" : { "$search" : "LEO",
"$caseSensitive": false,
"$diacriticSensitive": false }} )
.count() // 0 results
MongoDB 文档:
在 MongoDB 3.4 中,text search feature is designed to support case-insensitive searches on text content with language-specific rules for stopwords and stemming. Stemming rules for supported languages 基于通常处理常见动词和名词但不知道专有名词的标准算法。
没有明确支持部分匹配或模糊匹配,但源于相似结果的术语似乎可以正常工作。例如:"taste"、"tastes" 和 tasteful" 都词干为 "tast"。试试 Snowball Stemming Demo 页面来试验更多词和词干算法。
您的匹配结果是同一个词 "LEONEL" 的所有变体,并且仅因大小写和变音符号而异。除非 "LEONEL" 可以根据您选择的语言的规则缩短为更短的内容,否则这些是唯一匹配的变体类型。
如果您想进行高效的部分匹配,您需要采用不同的方法。有关一些有用的想法,请参阅:
- Efficient Techniques for Fuzzy and Partial matching in MongoDB 作者:约翰·佩奇
- Efficient Partial Keyword Searches 作者:James Tan
在 MongoDB 问题跟踪器中,您可以 watch/upvote 提出相关的改进请求:SERVER-15090: Improve Text Indexes to support partial word match。
import re
db.collection.find({"$or": [{"your field name": re.compile(text, re.IGNORECASE)},{"your field name": re.compile(text, re.IGNORECASE)}]})
由于Mongo目前默认不支持部分搜索...
我创建了一个简单的静态方法。
import mongoose from 'mongoose'
const PostSchema = new mongoose.Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
});
PostSchema.index({ title: "text", body: "text",},
{ weights: { title: 5, body: 3, } })
PostSchema.statics = {
searchPartial: function(q, callback) {
return this.find({
$or: [
{ "title": new RegExp(q, "gi") },
{ "body": new RegExp(q, "gi") },
]
}, callback);
},
searchFull: function (q, callback) {
return this.find({
$text: { $search: q, $caseSensitive: false }
}, callback)
},
search: function(q, callback) {
this.searchFull(q, (err, data) => {
if (err) return callback(err, data);
if (!err && data.length) return callback(err, data);
if (!err && data.length === 0) return this.searchPartial(q, callback);
});
},
}
export default mongoose.models.Post || mongoose.model('Post', PostSchema)
使用方法:
import Post from '../models/post'
Post.search('Firs', function(err, data) {
console.log(data);
})
不创建索引,我们可以简单地使用:
db.users.find({ name: /<full_or_partial_text>/i})
(不区分大小写)
我将@Ricardo Canelas 的回答包装在猫鼬插件中 on npm
进行了两项更改:
- 使用承诺
- 搜索类型为 String
的任何字段
这里是重要的源代码:
// mongoose-partial-full-search
module.exports = exports = function addPartialFullSearch(schema, options) {
schema.statics = {
...schema.statics,
makePartialSearchQueries: function (q) {
if (!q) return {};
const $or = Object.entries(this.schema.paths).reduce((queries, [path, val]) => {
val.instance == "String" &&
queries.push({
[path]: new RegExp(q, "gi")
});
return queries;
}, []);
return { $or }
},
searchPartial: function (q, opts) {
return this.find(this.makePartialSearchQueries(q), opts);
},
searchFull: function (q, opts) {
return this.find({
$text: {
$search: q
}
}, opts);
},
search: function (q, opts) {
return this.searchFull(q, opts).then(data => {
return data.length ? data : this.searchPartial(q, opts);
});
}
}
}
exports.version = require('../package').version;
用法
// PostSchema.js
import addPartialFullSearch from 'mongoose-partial-full-search';
PostSchema.plugin(addPartialFullSearch);
// some other file.js
import Post from '../wherever/models/post'
Post.search('Firs').then(data => console.log(data);)
如果您使用变量来存储要搜索的字符串或值:
它将与 Regex 一起工作,如:
{ collection.find({ name of Mongodb field: new RegExp(variable_name, 'i') }
这里,I 代表忽略大小写选项
对我有用的快速而肮脏的解决方案:首先使用文本搜索,如果未找到任何内容,然后使用正则表达式进行另一个查询。如果您不想进行两次查询 - $or
也可以,但是 requires all fields in query to be indexed.
另外,你最好不要使用case-insensitive rx,因为it can't rely on indexes。在我的例子中,我制作了已用字段的小写副本。
基于 n-gram 的模糊匹配方法在此处进行了说明
(还解释了如何使用前缀匹配为结果得分更高)
https://medium.com/xeneta/fuzzy-search-with-mongodb-and-python-57103928ee5d
注意:n-gram 基于方法的存储空间很大,mongodb 集合大小会增加。
如果您想利用 MongoDB 的全文搜索的所有优势并且想要部分匹配(可能用于自动完成),Shrikant Prabhu 提到的基于 n-gram 的方法是正确的解决方案为了我。显然你的里程可能会有所不同,这在索引大量文档时可能不切实际。
在我的例子中,我主要需要部分匹配来处理我文档的 title
字段(以及其他一些短字段)。
我使用了边缘 n-gram 方法。那是什么意思?简而言之,您将 "Mississippi River"
这样的字符串变成 "Mis Miss Missi Missis Mississ Mississi Mississip Mississipp Mississippi Riv Rive River"
.
这样的字符串
受到刘根this code的启发,想出了这个方法:
function createEdgeNGrams(str) {
if (str && str.length > 3) {
const minGram = 3
const maxGram = str.length
return str.split(" ").reduce((ngrams, token) => {
if (token.length > minGram) {
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
ngrams = [...ngrams, token.substr(0, i)]
}
} else {
ngrams = [...ngrams, token]
}
return ngrams
}, []).join(" ")
}
return str
}
let res = createEdgeNGrams("Mississippi River")
console.log(res)
现在,为了在 Mongo 中使用它,我将一个 searchTitle
字段添加到我的文档中,并通过将实际的 title
字段转换为边缘 n-gram 来设置它的值以上功能。我还为 searchTitle
字段创建了一个 "text"
索引。
然后我使用投影从搜索结果中排除 searchTitle
字段:
db.collection('my-collection')
.find({ $text: { $search: mySearchTerm } }, { projection: { searchTitle: 0 } })
full/partial 在 MongodB 中搜索“纯”流星项目
我将 flash 的代码与 Meteor-Collections 和 simpleSchema 一起使用,但没有猫鼬(意味着:删除 .plugin()
-method 和 schema.path
的使用(尽管看起来是一个 simpleSchema- flash 代码中的属性,它没有为我解析))并返回结果数组而不是游标。
认为这可能对某人有所帮助,所以我分享它。
export function partialFullTextSearch(meteorCollection, searchString) {
// builds an "or"-mongoDB-query for all fields with type "String" with a regEx as search parameter
const makePartialSearchQueries = () => {
if (!searchString) return {};
const $or = Object.entries(meteorCollection.simpleSchema().schema())
.reduce((queries, [name, def]) => {
def.type.definitions.some(t => t.type === String) &&
queries.push({[name]: new RegExp(searchString, "gi")});
return queries
}, []);
return {$or}
};
// returns a promise with result as array
const searchPartial = () => meteorCollection.rawCollection()
.find(makePartialSearchQueries(searchString)).toArray();
// returns a promise with result as array
const searchFull = () => meteorCollection.rawCollection()
.find({$text: {$search: searchString}}).toArray();
return searchFull().then(result => {
if (result.length === 0) throw null
else return result
}).catch(() => searchPartial());
}
这个 return 是一个 Promise,所以可以这样称呼它(即作为服务器端异步 Meteor-Method searchContact
的 return)。
这意味着您在调用此方法之前已将 simpleSchema 附加到您的集合。
return partialFullTextSearch(Contacts, searchString).then(result => result);
我创建了一个附加字段,它结合了我要搜索的文档中的所有字段。然后我只使用正则表达式:
user = {
firstName: 'Bob',
lastName: 'Smith',
address: {
street: 'First Ave',
city: 'New York City',
}
notes: 'Bob knows Mary'
}
// add combined search field with '+' separator to preserve spaces
user.searchString = `${user.firstName}+${user.lastName}+${user.address.street}+${user.address.city}+${user.notes}`
db.users.find({searchString: {$regex: 'mar', $options: 'i'}})
// returns Bob because 'mar' matches his notes field
// TODO write a client-side function to highlight the matching fragments
环境:
- MongoDB (3.2.0) 与猫鼬
Collection:
- 用户
创建文本索引:
BasicDBObject keys = new BasicDBObject();
keys.put("name","text");
BasicDBObject options = new BasicDBObject();
options.put("name", "userTextSearch");
options.put("unique", Boolean.FALSE);
options.put("background", Boolean.TRUE);
userCollection.createIndex(keys, options); // using MongoTemplate
文档:
- {"姓名":"莱昂内尔"}
查询:
db.users.find( { "$text" : { "$search" : "LEONEL" } } )
=> 找到db.users.find( { "$text" : { "$search" : "leonel" } } )
=> FOUND(搜索区分大小写为 false)db.users.find( { "$text" : { "$search" : "LEONÉL" } } )
=> 已找到(使用 diacriticSensitive 搜索是错误的)db.users.find( { "$text" : { "$search" : "LEONE" } } )
=> 已找到(部分搜索)db.users.find( { "$text" : { "$search" : "LEO" } } )
=> 未找到(部分搜索)db.users.find( { "$text" : { "$search" : "L" } } )
=> 未找到(部分搜索)
知道为什么我使用查询“LEO”或“L”得到 0 个结果吗?
不允许使用带有文本索引搜索的正则表达式。
db.getCollection('users')
.find( { "$text" : { "$search" : "/LEO/i",
"$caseSensitive": false,
"$diacriticSensitive": false }} )
.count() // 0 results
db.getCollection('users')
.find( { "$text" : { "$search" : "LEO",
"$caseSensitive": false,
"$diacriticSensitive": false }} )
.count() // 0 results
MongoDB 文档:
在 MongoDB 3.4 中,text search feature is designed to support case-insensitive searches on text content with language-specific rules for stopwords and stemming. Stemming rules for supported languages 基于通常处理常见动词和名词但不知道专有名词的标准算法。
没有明确支持部分匹配或模糊匹配,但源于相似结果的术语似乎可以正常工作。例如:"taste"、"tastes" 和 tasteful" 都词干为 "tast"。试试 Snowball Stemming Demo 页面来试验更多词和词干算法。
您的匹配结果是同一个词 "LEONEL" 的所有变体,并且仅因大小写和变音符号而异。除非 "LEONEL" 可以根据您选择的语言的规则缩短为更短的内容,否则这些是唯一匹配的变体类型。
如果您想进行高效的部分匹配,您需要采用不同的方法。有关一些有用的想法,请参阅:
- Efficient Techniques for Fuzzy and Partial matching in MongoDB 作者:约翰·佩奇
- Efficient Partial Keyword Searches 作者:James Tan
在 MongoDB 问题跟踪器中,您可以 watch/upvote 提出相关的改进请求:SERVER-15090: Improve Text Indexes to support partial word match。
import re
db.collection.find({"$or": [{"your field name": re.compile(text, re.IGNORECASE)},{"your field name": re.compile(text, re.IGNORECASE)}]})
由于Mongo目前默认不支持部分搜索...
我创建了一个简单的静态方法。
import mongoose from 'mongoose'
const PostSchema = new mongoose.Schema({
title: { type: String, default: '', trim: true },
body: { type: String, default: '', trim: true },
});
PostSchema.index({ title: "text", body: "text",},
{ weights: { title: 5, body: 3, } })
PostSchema.statics = {
searchPartial: function(q, callback) {
return this.find({
$or: [
{ "title": new RegExp(q, "gi") },
{ "body": new RegExp(q, "gi") },
]
}, callback);
},
searchFull: function (q, callback) {
return this.find({
$text: { $search: q, $caseSensitive: false }
}, callback)
},
search: function(q, callback) {
this.searchFull(q, (err, data) => {
if (err) return callback(err, data);
if (!err && data.length) return callback(err, data);
if (!err && data.length === 0) return this.searchPartial(q, callback);
});
},
}
export default mongoose.models.Post || mongoose.model('Post', PostSchema)
使用方法:
import Post from '../models/post'
Post.search('Firs', function(err, data) {
console.log(data);
})
不创建索引,我们可以简单地使用:
db.users.find({ name: /<full_or_partial_text>/i})
(不区分大小写)
我将@Ricardo Canelas 的回答包装在猫鼬插件中 on npm
进行了两项更改:
- 使用承诺
- 搜索类型为 String
这里是重要的源代码:
// mongoose-partial-full-search
module.exports = exports = function addPartialFullSearch(schema, options) {
schema.statics = {
...schema.statics,
makePartialSearchQueries: function (q) {
if (!q) return {};
const $or = Object.entries(this.schema.paths).reduce((queries, [path, val]) => {
val.instance == "String" &&
queries.push({
[path]: new RegExp(q, "gi")
});
return queries;
}, []);
return { $or }
},
searchPartial: function (q, opts) {
return this.find(this.makePartialSearchQueries(q), opts);
},
searchFull: function (q, opts) {
return this.find({
$text: {
$search: q
}
}, opts);
},
search: function (q, opts) {
return this.searchFull(q, opts).then(data => {
return data.length ? data : this.searchPartial(q, opts);
});
}
}
}
exports.version = require('../package').version;
用法
// PostSchema.js
import addPartialFullSearch from 'mongoose-partial-full-search';
PostSchema.plugin(addPartialFullSearch);
// some other file.js
import Post from '../wherever/models/post'
Post.search('Firs').then(data => console.log(data);)
如果您使用变量来存储要搜索的字符串或值:
它将与 Regex 一起工作,如:
{ collection.find({ name of Mongodb field: new RegExp(variable_name, 'i') }
这里,I 代表忽略大小写选项
对我有用的快速而肮脏的解决方案:首先使用文本搜索,如果未找到任何内容,然后使用正则表达式进行另一个查询。如果您不想进行两次查询 - $or
也可以,但是 requires all fields in query to be indexed.
另外,你最好不要使用case-insensitive rx,因为it can't rely on indexes。在我的例子中,我制作了已用字段的小写副本。
基于 n-gram 的模糊匹配方法在此处进行了说明 (还解释了如何使用前缀匹配为结果得分更高) https://medium.com/xeneta/fuzzy-search-with-mongodb-and-python-57103928ee5d
注意:n-gram 基于方法的存储空间很大,mongodb 集合大小会增加。
如果您想利用 MongoDB 的全文搜索的所有优势并且想要部分匹配(可能用于自动完成),Shrikant Prabhu 提到的基于 n-gram 的方法是正确的解决方案为了我。显然你的里程可能会有所不同,这在索引大量文档时可能不切实际。
在我的例子中,我主要需要部分匹配来处理我文档的 title
字段(以及其他一些短字段)。
我使用了边缘 n-gram 方法。那是什么意思?简而言之,您将 "Mississippi River"
这样的字符串变成 "Mis Miss Missi Missis Mississ Mississi Mississip Mississipp Mississippi Riv Rive River"
.
受到刘根this code的启发,想出了这个方法:
function createEdgeNGrams(str) {
if (str && str.length > 3) {
const minGram = 3
const maxGram = str.length
return str.split(" ").reduce((ngrams, token) => {
if (token.length > minGram) {
for (let i = minGram; i <= maxGram && i <= token.length; ++i) {
ngrams = [...ngrams, token.substr(0, i)]
}
} else {
ngrams = [...ngrams, token]
}
return ngrams
}, []).join(" ")
}
return str
}
let res = createEdgeNGrams("Mississippi River")
console.log(res)
现在,为了在 Mongo 中使用它,我将一个 searchTitle
字段添加到我的文档中,并通过将实际的 title
字段转换为边缘 n-gram 来设置它的值以上功能。我还为 searchTitle
字段创建了一个 "text"
索引。
然后我使用投影从搜索结果中排除 searchTitle
字段:
db.collection('my-collection')
.find({ $text: { $search: mySearchTerm } }, { projection: { searchTitle: 0 } })
full/partial 在 MongodB 中搜索“纯”流星项目
我将 flash 的代码与 Meteor-Collections 和 simpleSchema 一起使用,但没有猫鼬(意味着:删除 .plugin()
-method 和 schema.path
的使用(尽管看起来是一个 simpleSchema- flash 代码中的属性,它没有为我解析))并返回结果数组而不是游标。
认为这可能对某人有所帮助,所以我分享它。
export function partialFullTextSearch(meteorCollection, searchString) {
// builds an "or"-mongoDB-query for all fields with type "String" with a regEx as search parameter
const makePartialSearchQueries = () => {
if (!searchString) return {};
const $or = Object.entries(meteorCollection.simpleSchema().schema())
.reduce((queries, [name, def]) => {
def.type.definitions.some(t => t.type === String) &&
queries.push({[name]: new RegExp(searchString, "gi")});
return queries
}, []);
return {$or}
};
// returns a promise with result as array
const searchPartial = () => meteorCollection.rawCollection()
.find(makePartialSearchQueries(searchString)).toArray();
// returns a promise with result as array
const searchFull = () => meteorCollection.rawCollection()
.find({$text: {$search: searchString}}).toArray();
return searchFull().then(result => {
if (result.length === 0) throw null
else return result
}).catch(() => searchPartial());
}
这个 return 是一个 Promise,所以可以这样称呼它(即作为服务器端异步 Meteor-Method searchContact
的 return)。
这意味着您在调用此方法之前已将 simpleSchema 附加到您的集合。
return partialFullTextSearch(Contacts, searchString).then(result => result);
我创建了一个附加字段,它结合了我要搜索的文档中的所有字段。然后我只使用正则表达式:
user = {
firstName: 'Bob',
lastName: 'Smith',
address: {
street: 'First Ave',
city: 'New York City',
}
notes: 'Bob knows Mary'
}
// add combined search field with '+' separator to preserve spaces
user.searchString = `${user.firstName}+${user.lastName}+${user.address.street}+${user.address.city}+${user.notes}`
db.users.find({searchString: {$regex: 'mar', $options: 'i'}})
// returns Bob because 'mar' matches his notes field
// TODO write a client-side function to highlight the matching fragments