Skip to content

Commit 3f9e398

Browse files
committed
Add support for search tokenizers
1 parent d313d97 commit 3f9e398

6 files changed

Lines changed: 64 additions & 8 deletions

File tree

src/Extensions/ParadeDbIndexBuilderExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ public Bm25IndexBuilder<TEntity> IsCreatedConcurrently(bool createdConcurrently
138138
return this;
139139
}
140140

141+
public Bm25IndexBuilder<TEntity> HasSearchTokenizer(Tokenizer tokenizer)
142+
{
143+
_indexBuilder.HasAnnotation(
144+
ParadeDbAnnotationNames.Bm25SearchTokenizer,
145+
tokenizer.ToSearchString()
146+
);
147+
148+
return this;
149+
}
150+
141151
private void AddField(string field, string kind, Tokenizer? tokenizer, string? @alias)
142152
{
143153
var properties = GetAnnotation(ParadeDbAnnotationNames.Bm25FieldProperties);

src/Internal/Metadata/ParadeDbAnnotationNames.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ internal static class ParadeDbAnnotationNames
77
public const string Bm25FieldKinds = "ParadeDB:Bm25FieldKinds";
88
public const string Bm25FieldTokenizers = "ParadeDB:Bm25FieldTokenizers";
99
public const string Bm25FieldAliases = "ParadeDB:Bm25FieldAliases";
10+
public const string Bm25SearchTokenizer = "ParadeDB:Bm25SearchTokenizer";
1011
public const string Bm25KeyField = "ParadeDB:Bm25KeyField";
1112
public const string Bm25Fields = "ParadeDB:Bm25Fields";
1213
}

src/Internal/Metadata/ParadeDbAnnotationProvider.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,17 @@ is not string keyPropertyName
8181
)
8282
.ToArray()
8383
);
84+
85+
if (
86+
mappedIndex.FindAnnotation(ParadeDbAnnotationNames.Bm25SearchTokenizer)?.Value
87+
is string searchTokenizer
88+
)
89+
{
90+
yield return new Annotation(
91+
ParadeDbAnnotationNames.Bm25SearchTokenizer,
92+
searchTokenizer
93+
);
94+
}
8495
}
8596

8697
private static string RenderField(

src/Internal/Migrations/ParadeDbMigrationsSqlGenerator.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ is not string keyField
3535
}
3636

3737
var helper = Dependencies.SqlGenerationHelper;
38+
var stringMapping = Dependencies.TypeMappingSource.FindMapping(typeof(string))!;
3839
var createdConcurrently =
3940
operation.FindAnnotation(NpgsqlAnnotationNames.CreatedConcurrently)?.Value is true;
41+
var searchTokenizer =
42+
operation.FindAnnotation(ParadeDbAnnotationNames.Bm25SearchTokenizer)?.Value as string;
4043

4144
builder
4245
.Append("CREATE INDEX ")
@@ -47,12 +50,16 @@ is not string keyField
4750
.Append(" USING bm25 (")
4851
.Append(string.Join(", ", fields))
4952
.Append(") WITH (key_field = ")
50-
.Append(
51-
Dependencies
52-
.TypeMappingSource.FindMapping(typeof(string))!
53-
.GenerateSqlLiteral(keyField)
54-
)
55-
.Append(")");
53+
.Append(stringMapping.GenerateSqlLiteral(keyField));
54+
55+
if (searchTokenizer is not null)
56+
{
57+
builder
58+
.Append(", search_tokenizer = ")
59+
.Append(stringMapping.GenerateSqlLiteral(searchTokenizer));
60+
}
61+
62+
builder.Append(")");
5663

5764
if (operation.Filter is not null)
5865
{

src/Tokenizer.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,19 @@ public override string ToString()
4141
return args.Count == 0 ? $"pdb.{_name}" : $"pdb.{_name}({string.Join(",", args)})";
4242
}
4343

44+
internal string ToSearchString()
45+
{
46+
var args = _args.Concat(
47+
_options.Select(option =>
48+
$"{option.Key}={FormatSearchOptionValue(option.Value, option.Key)}"
49+
)
50+
);
51+
52+
var argsArray = args as string[] ?? args.ToArray();
53+
54+
return argsArray.Length == 0 ? _name : $"{_name}({string.Join(",", argsArray)})";
55+
}
56+
4457
private static string FormatOptionValue(object value, string key) =>
4558
value switch
4659
{
@@ -54,6 +67,19 @@ private static string FormatOptionValue(object value, string key) =>
5467
),
5568
};
5669

70+
private static string FormatSearchOptionValue(object value, string key) =>
71+
value switch
72+
{
73+
bool b => b.ToString().ToLowerInvariant(),
74+
string s => s,
75+
int n => n.ToString(CultureInfo.InvariantCulture),
76+
float n => n.ToString(CultureInfo.InvariantCulture),
77+
_ => throw new ArgumentException(
78+
$"Tokenizer option '{key}' has unsupported value type '{value?.GetType().Name}'. Each tokenizer option value must be a bool, string, int, or float.",
79+
"options"
80+
),
81+
};
82+
5783
private static string Quote(string value) => value.Replace("'", "''");
5884

5985
public static Tokenizer Unicode(Dictionary<string, object>? options = null) =>

tests/IndexingTest.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
8080
)
8181
.HasField(e => e.Rating, new FieldAlias("my_rating_alias"))
8282
.HasField("rating + 1", new FieldAlias("escape' me"))
83-
.HasFilter("rating > 0");
83+
.HasFilter("rating > 0")
84+
.HasSearchTokenizer(Tokenizer.Simple(new() { ["lowercase"] = false }));
8485
});
8586
}
8687
}
@@ -105,7 +106,7 @@ rating int
105106

106107
sql.ShouldBe(
107108
"""
108-
CREATE INDEX indexing_items_idx ON indexing_items USING bm25 (id, (description::pdb.ngram(3,3,'positions=true')), ((metadata ->> 'color')::pdb.literal('alias=metadata_color')), (rating::pdb.alias('my_rating_alias')), ((rating + 1)::pdb.alias('escape'' me'))) WITH (key_field = 'id') WHERE rating > 0;
109+
CREATE INDEX indexing_items_idx ON indexing_items USING bm25 (id, (description::pdb.ngram(3,3,'positions=true')), ((metadata ->> 'color')::pdb.literal('alias=metadata_color')), (rating::pdb.alias('my_rating_alias')), ((rating + 1)::pdb.alias('escape'' me'))) WITH (key_field = 'id', search_tokenizer = 'simple(lowercase=false)') WHERE rating > 0;
109110
110111
"""
111112
);

0 commit comments

Comments
 (0)