mirror of
https://github.com/papra-hq/papra.git
synced 2026-01-31 20:49:31 -06:00
feat(search): case-insensitive tag filtering (#827)
This commit is contained in:
committed by
GitHub
parent
494aa5b882
commit
ca2ef2866b
5
.changeset/cruel-bugs-sip.md
Normal file
5
.changeset/cruel-bugs-sip.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@papra/app": patch
|
||||
---
|
||||
|
||||
In the search queries, tag filters are now case-insensitive, so `tag:Important` and `tag:important` will match the same tag (as tags names are case-insensitive).
|
||||
@@ -37,7 +37,7 @@ function buildTagFilterCondition({ expression }: { expression: FilterExpression
|
||||
return falseCondition;
|
||||
}
|
||||
|
||||
return ({ document }) => document.tags.find(tag => tag.name === value || tag.id === value) !== undefined;
|
||||
return ({ document }) => document.tags.find(tag => tag.name.toLowerCase() === value.toLowerCase() || tag.id === value) !== undefined;
|
||||
}
|
||||
|
||||
function buildNameFilterCondition({ expression }: { expression: FilterExpression }): DocumentCondition {
|
||||
|
||||
@@ -41,16 +41,14 @@ describe('database-fts5 repository models', () => {
|
||||
|
||||
expect(issues).to.eql([]);
|
||||
expect(getSqlString(searchWhereClause)).to.eql({
|
||||
sql: `(\"documents\".\"organization_id\" = ? and \"documents\".\"is_deleted\" = ? and ((\"documents\".\"id\" in (select distinct \"documents_tags\".\"document_id\" from \"documents_tags\" inner join \"tags\" on \"documents_tags\".\"tag_id\" = \"tags\".\"id\" where (\"tags\".\"organization_id\" = ? and (\"tags\".\"name\" = ? or \"tags\".\"id\" = ?))) or \"documents\".\"id\" in (select distinct \"documents_tags\".\"document_id\" from \"documents_tags\" inner join \"tags\" on \"documents_tags\".\"tag_id\" = \"tags\".\"id\" where (\"tags\".\"organization_id\" = ? and (\"tags\".\"name\" = ? or \"tags\".\"id\" = ?)))) and not \"documents\".\"id\" in (select distinct \"document_id\" from \"documents_fts\" where \"documents_fts\" = ?)))`,
|
||||
sql: '("documents"."organization_id" = ? and "documents"."is_deleted" = ? and (("documents"."id" in (select distinct "documents_tags"."document_id" from "documents_tags" inner join "tags" on "documents_tags"."tag_id" = "tags"."id" where ("tags"."organization_id" = ? and "tags"."normalized_name" = ?)) or "documents"."id" in (select distinct "documents_tags"."document_id" from "documents_tags" inner join "tags" on "documents_tags"."tag_id" = "tags"."id" where ("tags"."organization_id" = ? and "tags"."normalized_name" = ?))) and not "documents"."id" in (select distinct "document_id" from "documents_fts" where "documents_fts" = ?)))',
|
||||
params: [
|
||||
'org_1',
|
||||
0,
|
||||
'org_1',
|
||||
'important',
|
||||
'important',
|
||||
'org_1',
|
||||
'urgent',
|
||||
'urgent',
|
||||
'organization_id:\"org_1\" {name content}:\"confidential\"*',
|
||||
],
|
||||
});
|
||||
|
||||
@@ -305,6 +305,10 @@ describe('database-fts5 repository', () => {
|
||||
searchQuery: 'tag:car tag:invoice',
|
||||
expectedDocumentsIds: ['doc_3'],
|
||||
},
|
||||
{
|
||||
searchQuery: 'tag:cAr tag:iNvoIce',
|
||||
expectedDocumentsIds: ['doc_3'],
|
||||
},
|
||||
{
|
||||
searchQuery: 'tag:car NOT contract',
|
||||
expectedDocumentsIds: ['doc_3'],
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
handleNameFilter,
|
||||
handleNotExpression,
|
||||
handleOrExpression,
|
||||
handleTagFilter,
|
||||
handleTextExpression,
|
||||
handleUnsupportedExpression,
|
||||
} from './query-builder';
|
||||
@@ -506,4 +507,36 @@ describe('query-builder', async () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleTagFilter', () => {
|
||||
test('when the tag filter value is a proper tag id, the built query matches by id', () => {
|
||||
const { sqlQuery, issues } = handleTagFilter({
|
||||
expression: { type: 'filter', field: 'tag', value: 'tag_123456789123456789123456', operator: '=' },
|
||||
organizationId: 'org_1',
|
||||
db,
|
||||
});
|
||||
|
||||
expect(issues).to.eql([]);
|
||||
|
||||
expect(getSqlString(sqlQuery)).to.eql({
|
||||
sql: '"documents"."id" in (select distinct "documents_tags"."document_id" from "documents_tags" inner join "tags" on "documents_tags"."tag_id" = "tags"."id" where ("tags"."organization_id" = ? and "tags"."id" = ?))',
|
||||
params: ['org_1', 'tag_123456789123456789123456'],
|
||||
});
|
||||
});
|
||||
|
||||
test('when the tag filter value is not a proper tag id, the built query matches by name', () => {
|
||||
const { sqlQuery, issues } = handleTagFilter({
|
||||
expression: { type: 'filter', field: 'tag', value: 'Important THING', operator: '=' },
|
||||
organizationId: 'org_1',
|
||||
db,
|
||||
});
|
||||
|
||||
expect(issues).to.eql([]);
|
||||
|
||||
expect(getSqlString(sqlQuery)).to.eql({
|
||||
sql: '"documents"."id" in (select distinct "documents_tags"."document_id" from "documents_tags" inner join "tags" on "documents_tags"."tag_id" = "tags"."id" where ("tags"."organization_id" = ? and "tags"."normalized_name" = ?))',
|
||||
params: ['org_1', 'important thing'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import type { Database } from '../../../../app/database/database.types';
|
||||
import type { QueryResult } from './query-builder.types';
|
||||
import { and, eq, inArray, not, or, sql } from 'drizzle-orm';
|
||||
import { isValidDate } from '../../../../shared/date';
|
||||
import { tagIdRegex } from '../../../../tags/tags.constants';
|
||||
import { normalizeTagName } from '../../../../tags/tags.repository.models';
|
||||
import { documentsTagsTable, tagsTable } from '../../../../tags/tags.table';
|
||||
import { documentsTable } from '../../../documents.table';
|
||||
import { documentsFtsTable } from '../database-fts5.tables';
|
||||
@@ -133,6 +135,10 @@ export function handleNotExpression({ expression, organizationId, db }: { expres
|
||||
export function handleTagFilter({ expression, organizationId, db }: { expression: FilterExpression; organizationId: string; db: Database }): QueryResult {
|
||||
const { value } = expression;
|
||||
|
||||
const query = tagIdRegex.test(value)
|
||||
? eq(tagsTable.id, value)
|
||||
: eq(tagsTable.normalizedName, normalizeTagName({ name: value }));
|
||||
|
||||
return {
|
||||
sqlQuery: inArray(
|
||||
documentsTable.id,
|
||||
@@ -142,10 +148,7 @@ export function handleTagFilter({ expression, organizationId, db }: { expression
|
||||
.where(and(
|
||||
// Ensure tag belongs to the same organization + helps performance as there is an index on (organizationId + name)
|
||||
eq(tagsTable.organizationId, organizationId),
|
||||
or(
|
||||
eq(tagsTable.name, value),
|
||||
eq(tagsTable.id, value),
|
||||
),
|
||||
query,
|
||||
)),
|
||||
),
|
||||
issues: [],
|
||||
|
||||
Reference in New Issue
Block a user