搜索多个部分短语,以便一个原始短语无法匹配多个搜索词组

时间:2015-05-08 12:22:18

标签: javascript algorithm data-structures autocomplete autosuggest

给定一组预定义的短语,我想根据用户的查询执行搜索。例如,请考虑以下一组短语:

index      phrase
-----------------------------------------
0          Stack Overflow
1          Math Overflow
2          Super User
3          Webmasters
4          Electrical Engineering
5          Programming Jokes
6          Programming Puzzles
7          Geographic Information Systems 

预期的行为是:

query         result
------------------------------------------------------------------------
s             Stack Overflow, Super User, Geographic Information Systems
web           Webmasters
over          Stack Overflow, Math Overflow
super u       Super User
user s        Super User
e e           Electrical Engineering
p             Programming Jokes, Programming Puzzles
p p           Programming Puzzles

实现此行为I used a trie。特里结构中的每个节点都有一个索引数组(最初为空)。

要在trie中插入一个短语,我首先将其分解为单词。例如,Programming Puzzlesindex = 6。因此,我将6添加到以下所有节点:

p
pr
pro
prog
progr
progra
program
programm
programmi
programmin
programming
pu
puz
puzz
puzzl
puzzle
puzzles

问题是,当我搜索查询prog p时,我首先得到prog的{​​{1}}索引列表。然后,我得到[5, 6]的索引列表,p。最后,我计算两者之间的交集,并返回结果[5, 6],这显然是错误的(应该是[5, 6])。

你会如何解决这个问题?

4 个答案:

答案 0 :(得分:4)

重点观察

我们可以使用查询中的两个单词可以匹配短语中相同单词的事实,前提是一个查询单词是另一个查询单词的前缀(或者如果它们是相同的)。因此,如果我们按照字母顺序排序顺序处理查询单词(前缀来自"超级字"),那么我们可以安全从第一次匹配的短语中删除单词。这样做我们就不可能两次匹配相同的短语。正如我所说,这是安全的,因为前缀匹配短语单词的超集,他们的"超级词"可以匹配,并且一对查询词,其中一个不是另一个的前缀,总是匹配不相交的一组词组词。

我们不必从短语中删除单词或者从物理上删除单词"我们可以实现"虚拟"。

算法的实现

var PhraseSearch = function () {   
    var Trie = function () {
        this.phraseWordCount = {};
        this.children = {};
    };

    Trie.prototype.addPhraseWord = function (phrase, word) {
        if (word !== '') {
            var first = word.charAt(0);

            if (!this.children.hasOwnProperty(first)) {
                this.children[first] = new Trie();
            }
            var rest = word.substring(1);
            this.children[first].addPhraseWord(phrase, rest);
        }
        if (!this.phraseWordCount.hasOwnProperty(phrase)) {
            this.phraseWordCount[phrase] = 0;
        }
        this.phraseWordCount[phrase]++;
    };

    Trie.prototype.getPhraseWordCount = function (prefix) {
        if (prefix !== '') {
            var first = prefix.charAt(0);

            if (this.children.hasOwnProperty(first)) {
                var rest = prefix.substring(1);
                return this.children[first].getPhraseWordCount(rest);
            } else {
                return {};
            }
        } else {
            return this.phraseWordCount;
        }
    }

    this.trie = new Trie();
}

PhraseSearch.prototype.addPhrase = function (phrase) {
    var words = phrase.trim().toLowerCase().split(/\s+/);
    words.forEach(function (word) {
        this.trie.addPhraseWord(phrase, word);
    }, this);
}

PhraseSearch.prototype.search = function (query) {
    var answer = {};
    var phraseWordCount = this.trie.getPhraseWordCount('');
    for (var phrase in phraseWordCount) {
        if (phraseWordCount.hasOwnProperty(phrase)) {
            answer[phrase] = true;
        }
    }

    var prefixes = query.trim().toLowerCase().split(/\s+/);

    prefixes.sort();
    prefixes.reverse();

    var prevPrefix = '';
    var superprefixCount = 0;

    prefixes.forEach(function (prefix) {
        if (prevPrefix.indexOf(prefix) !== 0) {
            superprefixCount = 0;
        }
        phraseWordCount = this.trie.getPhraseWordCount(prefix);

        function phraseMatchedWordCount(phrase) {
            return phraseWordCount.hasOwnProperty(phrase) ? phraseWordCount[phrase] - superprefixCount : 0;
        }

        for (var phrase in answer) {
            if (answer.hasOwnProperty(phrase) && phraseMatchedWordCount(phrase) < 1) {
                delete answer[phrase];
            }
        }

        prevPrefix = prefix;
        superprefixCount++;
    }, this);

    return Object.keys(answer);
}

function test() {
    var phraseSearch = new PhraseSearch();

    var phrases = [
        'Stack Overflow',
        'Math Overflow',
        'Super User',
        'Webmasters',
        'Electrical Engineering',
        'Programming Jokes',
        'Programming Puzzles',
        'Geographic Information Systems'
    ];

    phrases.forEach(phraseSearch.addPhrase, phraseSearch);

    var queries = {
        's':       'Stack Overflow, Super User, Geographic Information Systems',
        'web':     'Webmasters',
        'over':    'Stack Overflow, Math Overflow',
        'super u': 'Super User',
        'user s':  'Super User',
        'e e':     'Electrical Engineering',
        'p':       'Programming Jokes, Programming Puzzles',
        'p p':     'Programming Puzzles'
    };

    for(var query in queries) {
        if (queries.hasOwnProperty(query)) {
            var expected = queries[query];
            var actual = phraseSearch.search(query).join(', ');

            console.log('query: ' + query);
            console.log('expected: ' + expected);
            console.log('actual: ' + actual);
        }
    }
}

可以在此处测试此代码:http://ideone.com/RJgj6p

可能的优化

  • 在每个trie节点中存储短语字数不是很大的内存 高效。但通过实施compressed trie,有可能 将最坏情况下的内存复杂度降低到 O(n m) n 是 所有短语中不同单词的数量, m 是总数 短语数量。

  • 为简单起见,我通过添加所有短语来初始化answer。但 更有效的方法是通过添加来初始化answer 与匹配最少数量的查询词匹配的短语 短语。然后与查询词匹配的短语相交 第二少数短语。等等......

与问题中引用的the Implementation的相关差异

  1. 在trie节点中,我不仅存储与子集匹配的短语引用(ids),还存储这些短语中匹配单词的数量。因此,匹配的结果不仅是匹配的短语引用,还包括这些短语中匹配的单词的数量。
  2. 我按降序字典顺序处理查询字词。
  3. 我从当前匹配结果中减去超级前缀的数量(当前查询词是前缀的查询词)(通过使用变量superprefixCount),并且只有当前查询词时,短语被认为是匹配的结果中匹配的单词数量大于零。与原始实现一样,最终结果是匹配短语的交集。
  4. 可以看出,变化是微乎其微的,渐近的复杂性(时间和记忆)都没有改变。

答案 1 :(得分:3)

如果定义了一组短语并且不包含长短语,那么你可以创建不是1 trie,但是n次尝试,其中n是一个短语中的最大单词数。

在第i个商店第i个词中。我们称它为带有标签'i'的trie。

要使用m个单词处理查询,请考虑以下算法:

  1. 对于每个短语,我们将存储特里结构的最低标签,其中找到了该短语中的单词。我们将其表示为d [j],其中j是短语索引。首先,对于每个短语j,d [j] = -1。
  2. 搜索n次尝试中的第一个单词。
  3. 对于每个短语,j找到大于d [j]的trie的标签,并找到该短语中的单词。如果有几个这样的标签,请选择最小的标签。我们将这样的标签表示为c [j]。
  4. 如果没有此类索引,则无法匹配此短语。您可以使用d [j] = n + 1
  5. 标记此案例
  6. 如果存在c [j]> c [j]的c [j] d [j],而不是指定d [j] = c [j]。
  7. 重复每一个字。
  8. 每个短语-1&lt; d [j]&lt; n匹配。
  9. 这不是最优的。要提高性能,您应该只存储d数组的可用值。在第一个单词之后,只存储与此单词匹配的短语。另外,不是赋值d [j] = n + 1,而是删除索引j。仅处理已存储的短语索引。

答案 2 :(得分:2)

您可以在Graph Matching Problem中将其解析为Bipartite Graph

对于每个文档,查询对定义图形:

G=(V,E) Where
V = {t1 | for each term t1 in the query} U { t2 | for each term t2 in the document}
E = { (t1,t2) | t1 is a match for t2 }

直观地说:只有查询词与文档词匹配时,查询中的每个词都有一个顶点,文档中每个词的顶点以及文档词和查询词之间的边。你已经用你的特里解决了这个部分。

你有一个二分图,“查询顶点”和“文档顶点”之间只有边缘(而不是相同类型的两个顶点之间)。

现在,调用matching problem for bipartite graph,获得最佳匹配{(t1_1,t2_1), ... , (t1_k,t2_k)}

如果(并且仅当)满足所有d条款,您的算法应为查询中的q项查询m提交文档m,意味着 - 你有k=m的最大匹配。

在你的例子中,query =“prog p”的图形和document =“Programming Jokes”,你将得到匹配的二分图:(或编程,p匹配 - 无关紧要)< / p>

enter image description here

并且,对于相同的查询和document =“编程拼图”,您将获得具有匹配的二分图:

enter image description here

正如您所看到的,对于第一个示例 - 没有涵盖所有术语的匹配,您将“拒绝”该文档。对于第二个示例 - 您可以匹配所有条款,并且您将返回它。

对于性能问题,您可以仅对短语的子集执行建议的算法,这些短语已经通过初始方法过滤掉(所有术语匹配的文档的交集)。

答案 3 :(得分:1)

经过一番思考后,我想出了一个类似于dened的想法 - 除了匹配短语的索引之外,每个前缀都会引用它作为该短语中前缀的单词数 - 然后该数字可以减少查询过程中其超级修正符的数量以及其他查询词,并且返回的结果仅包括与查询匹配的词数至少相同的那些。

我们可以通过添加(对于英语)最多大约26选择2 + 26选择3甚至另外26选择4个特殊元素来引用trie来实现额外的小调整以避免大量交叉检查订购了第一个字母的交叉点。插入短语时,trie中引用2和3个第一个字母组合的特殊元素将接收其索引。然后可以对这些较大查询词的匹配结果进行交叉检查。例如,如果我们的查询为“Geo i”,则"Geo"的匹配结果将与特殊的trie元素"g-i"进行交叉检查,这有望显着减少匹配结果"i"

此外,根据具体情况,有时可以更有效地并行处理大量交叉检查(例如,通过bitset&amp;)。