mirror of https://github.com/Squidex/squidex.git
Browse Source
* Eliminate stop words before search. * Improve tokenization. * Fix tests * Update fluid * Cleanup.pull/952/head
committed by
GitHub
21 changed files with 513 additions and 350 deletions
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Fluid; |
||||
|
using Fluid.Ast; |
||||
|
using Parlot.Fluent; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Templates; |
||||
|
|
||||
|
public sealed class CustomFluidParser : FluidParser |
||||
|
{ |
||||
|
public Deferred<Expression> PrimaryParser => Primary; |
||||
|
|
||||
|
public Parser<char> CommaParser => Comma; |
||||
|
} |
||||
@ -0,0 +1,144 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Reflection; |
||||
|
using Lucene.Net.Analysis; |
||||
|
using Lucene.Net.Analysis.Core; |
||||
|
using Lucene.Net.Analysis.Standard; |
||||
|
using Lucene.Net.Analysis.TokenAttributes; |
||||
|
using Lucene.Net.Analysis.Util; |
||||
|
using Lucene.Net.Util; |
||||
|
using Squidex.Domain.Apps.Core; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.ObjectPool; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.MongoDb.Text; |
||||
|
|
||||
|
public static class Tokenizer |
||||
|
{ |
||||
|
private const LuceneVersion Version = LuceneVersion.LUCENE_48; |
||||
|
private static readonly Dictionary<string, CharArraySet> StopWords = new Dictionary<string, CharArraySet>(StringComparer.OrdinalIgnoreCase); |
||||
|
|
||||
|
static Tokenizer() |
||||
|
{ |
||||
|
foreach (var type in typeof(StandardAnalyzer).Assembly.GetTypes()) |
||||
|
{ |
||||
|
if (!typeof(Analyzer).IsAssignableFrom(type)) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
var language = type.Namespace!.Split('.')[^1]; |
||||
|
|
||||
|
if (language.Length != 2) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
var stopWordMethod = |
||||
|
type.GetProperties(BindingFlags.Public | BindingFlags.Static) |
||||
|
.FirstOrDefault(x => x.PropertyType == typeof(CharArraySet)); |
||||
|
|
||||
|
if (stopWordMethod != null) |
||||
|
{ |
||||
|
var stopWords = (CharArraySet)stopWordMethod.GetValue(null)!; |
||||
|
|
||||
|
StopWords[language] = stopWords; |
||||
|
} |
||||
|
} |
||||
|
catch (MissingMethodException) |
||||
|
{ |
||||
|
continue; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public static string TokenizeQuery(string query) |
||||
|
{ |
||||
|
query = query.Trim(); |
||||
|
|
||||
|
var textLanguage = Language.EN.Iso2Code; |
||||
|
var textReader = new StringReader(query); |
||||
|
|
||||
|
if (query.Length > 2 && query[2] == ':') |
||||
|
{ |
||||
|
textLanguage = query.Substring(0, 2); |
||||
|
textReader.Read(); |
||||
|
textReader.Read(); |
||||
|
} |
||||
|
|
||||
|
return TokenizeWord(textReader, textLanguage); |
||||
|
} |
||||
|
|
||||
|
public static string TokenizerTerms(string query, string language) |
||||
|
{ |
||||
|
var stopWords = |
||||
|
string.Equals(language, InvariantPartitioning.Key, StringComparison.OrdinalIgnoreCase) ? |
||||
|
null : |
||||
|
StopWords.GetValueOrDefault(language) ?? |
||||
|
StopWords["en"]; |
||||
|
|
||||
|
return Tokenize(new StringReader(query), stopWords); |
||||
|
} |
||||
|
|
||||
|
private static string TokenizeWord(TextReader reader, string language) |
||||
|
{ |
||||
|
var stopWords = |
||||
|
string.Equals(language, InvariantPartitioning.Key, StringComparison.OrdinalIgnoreCase) ? |
||||
|
null : |
||||
|
StopWords.GetValueOrDefault(language) ?? |
||||
|
StopWords["en"]; |
||||
|
|
||||
|
return Tokenize(reader, stopWords); |
||||
|
} |
||||
|
|
||||
|
private static string Tokenize(TextReader reader, CharArraySet? stopWords) |
||||
|
{ |
||||
|
var tokenizer = new StandardTokenizer(Version, reader); |
||||
|
var tokenStream = (TokenStream)new StandardFilter(Version, tokenizer); |
||||
|
|
||||
|
// Stop words are case sensitive, therefore we have to lowercase it first.
|
||||
|
tokenStream = new LowerCaseFilter(Version, tokenStream); |
||||
|
|
||||
|
if (stopWords != null) |
||||
|
{ |
||||
|
tokenStream = new StopFilter(Version, tokenStream, stopWords); |
||||
|
} |
||||
|
|
||||
|
var sb = DefaultPools.StringBuilder.Get(); |
||||
|
try |
||||
|
{ |
||||
|
// Actually not idea what this is doing, but it seems to work.
|
||||
|
var attribute = tokenStream.AddAttribute<ICharTermAttribute>(); |
||||
|
|
||||
|
tokenStream.Reset(); |
||||
|
|
||||
|
using (tokenStream) |
||||
|
{ |
||||
|
while (tokenStream.IncrementToken()) |
||||
|
{ |
||||
|
var text = attribute.ToString(); |
||||
|
|
||||
|
if (sb.Length > 0) |
||||
|
{ |
||||
|
sb.Append(' '); |
||||
|
} |
||||
|
|
||||
|
sb.Append(text); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return sb.ToString(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
DefaultPools.StringBuilder.Return(sb); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using Squidex.Domain.Apps.Entities.MongoDb.Text; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Entities.Contents.MongoDb; |
||||
|
|
||||
|
public class TokenizerTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_eliminate_stop_words_in_implicit_english() |
||||
|
{ |
||||
|
var source = "The only thing that matters, is time."; |
||||
|
|
||||
|
var parsed = Tokenizer.TokenizeQuery(source); |
||||
|
|
||||
|
Assert.Equal("only thing matters time", parsed); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_eliminate_stop_words_in_explicit_english() |
||||
|
{ |
||||
|
var source = "en:The only thing that matters, is time."; |
||||
|
|
||||
|
var parsed = Tokenizer.TokenizeQuery(source); |
||||
|
|
||||
|
Assert.Equal("only thing matters time", parsed); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_eliminate_stop_words_in_explicit_english2() |
||||
|
{ |
||||
|
var source = "en:when i do this it is pretty slow"; |
||||
|
|
||||
|
var parsed = Tokenizer.TokenizeQuery(source); |
||||
|
|
||||
|
Assert.Equal("when i do pretty slow", parsed); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_eliminate_stop_words_for_iv_language() |
||||
|
{ |
||||
|
var source = "iv:The only thing that matters, is time."; |
||||
|
|
||||
|
var parsed = Tokenizer.TokenizeQuery(source); |
||||
|
|
||||
|
Assert.Equal("the only thing that matters is time", parsed); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_eliminate_stop_words_in_explicit_german() |
||||
|
{ |
||||
|
var source = "de:Nur die Zeit spielt eine Rolle"; |
||||
|
|
||||
|
var parsed = Tokenizer.TokenizeQuery(source); |
||||
|
|
||||
|
Assert.Equal("zeit spielt rolle", parsed); |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue