MongoDB 全文和部分文本搜索

MongoDB Full and Partial Text Search

环境:


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

文档:


查询:

知道为什么我使用查询“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" 可以根据您选择的语言的规则缩短为更短的内容,否则这些是唯一匹配的变体类型。

如果您想进行高效的部分匹配,您需要采用不同的方法。有关一些有用的想法,请参阅:

在 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