Skip to content

Commit

Permalink
Adjust searchToMikroOrmQuery function to reduce the amount of irrel…
Browse files Browse the repository at this point in the history
…evant results (#2075)

This is done by a combination of an and- & or-query.
It makes a search more restrictive. For example a search of `Red Shirt`
won't give all products containing `red` or `shirt` but rather returns
all products that have the words `red` and `shirt` in some column. The
words don't have to be in the same column.

---------

Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com>
  • Loading branch information
jomunker and johnnyomair authored Jun 11, 2024
1 parent ebdd108 commit 07a7291
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 15 deletions.
7 changes: 7 additions & 0 deletions .changeset/tasty-garlics-repair.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@comet/cms-api": minor
---

Adjust `searchToMikroOrmQuery` function to reduce the amount of irrelevant results

This is done by using a combination of AND- and OR-queries. For example, a search of `red shirt` won't give all products containing `red` OR `shirt` but rather returns all products that have the words `red` AND `shirt` in some column. The words don't have to be in the same column.
78 changes: 68 additions & 10 deletions packages/api/cms-api/src/common/filter/mikro-orm.spec.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,96 @@
import { BooleanFilter } from "./boolean.filter";
import { DateFilter } from "./date.filter";
import { filtersToMikroOrmQuery, filterToMikroOrmQuery, searchToMikroOrmQuery } from "./mikro-orm";
import { filtersToMikroOrmQuery, filterToMikroOrmQuery, searchToMikroOrmQuery, splitSearchString } from "./mikro-orm";
import { NumberFilter } from "./number.filter";
import { StringFilter } from "./string.filter";

describe("splitSearchString", () => {
it("should split a simple space-separated string", () => {
const input = "This is a test";
const expected = ["%This%", "%is%", "%a%", "%test%"];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle quoted strings as single tokens", () => {
const input = 'This is a "quoted string"';
const expected = ["%This%", "%is%", "%a%", "%quoted string%"];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle escaped quotes within quoted strings", () => {
const input = 'This is a "quoted \\"string\\""';
const expected = ["%This%", "%is%", "%a%", '%quoted "string"%'];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle single quotes", () => {
const input = "This is a 'quoted string'";
const expected = ["%This%", "%is%", "%a%", "%quoted string%"];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle escaped quotes within single quoted strings", () => {
const input = "This is a 'quoted \\'string\\''";
const expected = ["%This%", "%is%", "%a%", "%quoted 'string'%"];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle mixed quotes", () => {
const input = "This \"is a\" 'test'";
const expected = ["%This%", "%is a%", "%test%"];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle empty strings", () => {
const input = "";
const expected: string[] = [];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle strings with special characters", () => {
const input = "This is a test with % and _ characters";
const expected = ["%This%", "%is%", "%a%", "%test%", "%with%", "%\\%%", "%and%", "%\\_%", "%characters%"];
expect(splitSearchString(input)).toEqual(expected);
});

it("should handle strings with only special characters", () => {
const input = "% _";
const expected = ["%\\%%", "%\\_%"];
expect(splitSearchString(input)).toEqual(expected);
});
});

describe("searchToMikroOrmQuery", () => {
it("should work", async () => {
expect(searchToMikroOrmQuery("foo", ["title", "description"])).toStrictEqual({
$or: [{ title: { $ilike: "%foo%" } }, { description: { $ilike: "%foo%" } }],
$and: [{ $or: [{ title: { $ilike: "%foo%" } }, { description: { $ilike: "%foo%" } }] }],
});
});
it("should escape %", async () => {
expect(searchToMikroOrmQuery("fo%o", ["title", "description"])).toStrictEqual({
$or: [{ title: { $ilike: "%fo\\%o%" } }, { description: { $ilike: "%fo\\%o%" } }],
$and: [{ $or: [{ title: { $ilike: "%fo\\%o%" } }, { description: { $ilike: "%fo\\%o%" } }] }],
});
});
it("should escape _", async () => {
expect(searchToMikroOrmQuery("fo_o", ["title", "description"])).toStrictEqual({
$or: [{ title: { $ilike: "%fo\\_o%" } }, { description: { $ilike: "%fo\\_o%" } }],
$and: [{ $or: [{ title: { $ilike: "%fo\\_o%" } }, { description: { $ilike: "%fo\\_o%" } }] }],
});
});
it("should split by spaces", async () => {
expect(searchToMikroOrmQuery("foo bar", ["title", "description"])).toStrictEqual({
$or: [
{ title: { $ilike: "%foo%" } },
{ title: { $ilike: "%bar%" } },
{ description: { $ilike: "%foo%" } },
{ description: { $ilike: "%bar%" } },
$and: [
{
$or: [{ title: { $ilike: "%foo%" } }, { description: { $ilike: "%foo%" } }],
},
{
$or: [{ title: { $ilike: "%bar%" } }, { description: { $ilike: "%bar%" } }],
},
],
});
});
it("should ignore leading and trailing spaces", async () => {
expect(searchToMikroOrmQuery(" a ", ["title"])).toStrictEqual({
$or: [{ title: { $ilike: "%a%" } }],
$and: [{ $or: [{ title: { $ilike: "%a%" } }] }],
});
});
});
Expand Down
13 changes: 8 additions & 5 deletions packages/api/cms-api/src/common/filter/mikro-orm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function filtersToMikroOrmQuery(
return genericFilter(filter);
}

const splitSearchString = (search: string) => {
export const splitSearchString = (search: string) => {
// regex to match all single tokens or quotes in a string => "This is a 'quoted string'" will result in ["This", "is", "a", "quoted string"]
// it will also take escaped quotes (prepended with a backslash => \) into account
const regex = /(["'])(?:(?=(\\?))\2.)*?\1|\S+/g;
Expand All @@ -169,9 +169,11 @@ const splitSearchString = (search: string) => {
export function searchToMikroOrmQuery(search: string, fields: string[]): ObjectQuery<any> {
const quotedSearchParts = splitSearchString(search);

const ors = [];
for (const field of fields) {
for (const quotedSearch of quotedSearchParts) {
const ands = [];

for (const quotedSearch of quotedSearchParts) {
const ors = [];
for (const field of fields) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const or: any = {};
let nestedFilter = or;
Expand All @@ -181,8 +183,9 @@ export function searchToMikroOrmQuery(search: string, fields: string[]): ObjectQ
nestedFilter.$ilike = quotedSearch;
ors.push(or);
}
ands.push({ $or: ors });
}
return {
$or: ors,
$and: ands,
};
}

0 comments on commit 07a7291

Please sign in to comment.