mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-02 11:09:20 +02:00
Fix latency caused by large numbers of tags
This commit is contained in:
parent
f63d0ca3ad
commit
dae4f6a0bd
@ -1,5 +1,6 @@
|
|||||||
from sqlalchemy import delete
|
from sqlalchemy import delete
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
from sqlalchemy import or_
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
@ -107,18 +108,28 @@ def create_or_add_document_tag_list(
|
|||||||
|
|
||||||
|
|
||||||
def get_tags_by_value_prefix_for_source_types(
|
def get_tags_by_value_prefix_for_source_types(
|
||||||
|
tag_key_prefix: str | None,
|
||||||
tag_value_prefix: str | None,
|
tag_value_prefix: str | None,
|
||||||
sources: list[DocumentSource] | None,
|
sources: list[DocumentSource] | None,
|
||||||
|
limit: int | None,
|
||||||
db_session: Session,
|
db_session: Session,
|
||||||
) -> list[Tag]:
|
) -> list[Tag]:
|
||||||
query = select(Tag)
|
query = select(Tag)
|
||||||
|
|
||||||
if tag_value_prefix:
|
if tag_key_prefix or tag_value_prefix:
|
||||||
query = query.where(Tag.tag_value.startswith(tag_value_prefix))
|
conditions = []
|
||||||
|
if tag_key_prefix:
|
||||||
|
conditions.append(Tag.tag_key.ilike(f"{tag_key_prefix}%"))
|
||||||
|
if tag_value_prefix:
|
||||||
|
conditions.append(Tag.tag_value.ilike(f"{tag_value_prefix}%"))
|
||||||
|
query = query.where(or_(*conditions))
|
||||||
|
|
||||||
if sources:
|
if sources:
|
||||||
query = query.where(Tag.source.in_(sources))
|
query = query.where(Tag.source.in_(sources))
|
||||||
|
|
||||||
|
if limit:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
result = db_session.execute(query)
|
result = db_session.execute(query)
|
||||||
|
|
||||||
tags = result.scalars().all()
|
tags = result.scalars().all()
|
||||||
|
@ -88,6 +88,7 @@ def get_tags(
|
|||||||
# If this is empty or None, then tags for all sources are considered
|
# If this is empty or None, then tags for all sources are considered
|
||||||
sources: list[DocumentSource] | None = None,
|
sources: list[DocumentSource] | None = None,
|
||||||
allow_prefix: bool = True, # This is currently the only option
|
allow_prefix: bool = True, # This is currently the only option
|
||||||
|
limit: int = 50,
|
||||||
_: User = Depends(current_user),
|
_: User = Depends(current_user),
|
||||||
db_session: Session = Depends(get_session),
|
db_session: Session = Depends(get_session),
|
||||||
) -> TagResponse:
|
) -> TagResponse:
|
||||||
@ -95,8 +96,10 @@ def get_tags(
|
|||||||
raise NotImplementedError("Cannot disable prefix match for now")
|
raise NotImplementedError("Cannot disable prefix match for now")
|
||||||
|
|
||||||
db_tags = get_tags_by_value_prefix_for_source_types(
|
db_tags = get_tags_by_value_prefix_for_source_types(
|
||||||
|
tag_key_prefix=match_pattern,
|
||||||
tag_value_prefix=match_pattern,
|
tag_value_prefix=match_pattern,
|
||||||
sources=sources,
|
sources=sources,
|
||||||
|
limit=limit,
|
||||||
db_session=db_session,
|
db_session=db_session,
|
||||||
)
|
)
|
||||||
server_tags = [
|
server_tags = [
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useChatContext } from "@/components/context/ChatContext";
|
import { useChatContext } from "@/components/context/ChatContext";
|
||||||
import { FilterManager } from "@/lib/hooks";
|
import { FilterManager } from "@/lib/hooks";
|
||||||
import { listSourceMetadata } from "@/lib/sources";
|
import { listSourceMetadata } from "@/lib/sources";
|
||||||
import { useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
DateRangePicker,
|
DateRangePicker,
|
||||||
DateRangePickerItem,
|
DateRangePickerItem,
|
||||||
@ -12,23 +12,46 @@ import { getXDaysAgo } from "@/lib/dateUtils";
|
|||||||
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
|
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
|
||||||
import { Bubble } from "@/components/Bubble";
|
import { Bubble } from "@/components/Bubble";
|
||||||
import { FiX } from "react-icons/fi";
|
import { FiX } from "react-icons/fi";
|
||||||
|
import { getValidTags } from "@/lib/tags/tagUtils";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { Tag } from "@/lib/types";
|
||||||
|
|
||||||
export function FiltersTab({
|
export function FiltersTab({
|
||||||
filterManager,
|
filterManager,
|
||||||
}: {
|
}: {
|
||||||
filterManager: FilterManager;
|
filterManager: FilterManager;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [filterValue, setFilterValue] = useState<string>("");
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const { availableSources, availableDocumentSets, availableTags } =
|
const { availableSources, availableDocumentSets, availableTags } =
|
||||||
useChatContext();
|
useChatContext();
|
||||||
|
|
||||||
|
const [filterValue, setFilterValue] = useState<string>("");
|
||||||
|
const [filteredTags, setFilteredTags] = useState<Tag[]>(availableTags);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const allSources = listSourceMetadata();
|
const allSources = listSourceMetadata();
|
||||||
const availableSourceMetadata = allSources.filter((source) =>
|
const availableSourceMetadata = allSources.filter((source) =>
|
||||||
availableSources.includes(source.internalName)
|
availableSources.includes(source.internalName)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const debouncedFetchTags = useRef(
|
||||||
|
debounce(async (value: string) => {
|
||||||
|
if (value) {
|
||||||
|
const fetchedTags = await getValidTags(value);
|
||||||
|
setFilteredTags(fetchedTags);
|
||||||
|
} else {
|
||||||
|
setFilteredTags(availableTags);
|
||||||
|
}
|
||||||
|
}, 50)
|
||||||
|
).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedFetchTags(filterValue);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedFetchTags.cancel();
|
||||||
|
};
|
||||||
|
}, [filterValue, availableTags, debouncedFetchTags]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden flex flex-col">
|
<div className="overflow-hidden flex flex-col">
|
||||||
<div className="overflow-y-auto">
|
<div className="overflow-y-auto">
|
||||||
@ -210,17 +233,15 @@ export function FiltersTab({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-h-48 flex flex-col gap-y-1 overflow-y-auto">
|
<div className="max-h-48 flex flex-col gap-y-1 overflow-y-auto">
|
||||||
{availableTags.length > 0 ? (
|
{filteredTags.length > 0 ? (
|
||||||
availableTags
|
filteredTags
|
||||||
.filter(
|
.filter(
|
||||||
(tag) =>
|
(tag) =>
|
||||||
!filterManager.selectedTags.some(
|
!filterManager.selectedTags.some(
|
||||||
(selectedTag) =>
|
(selectedTag) =>
|
||||||
selectedTag.tag_key === tag.tag_key &&
|
selectedTag.tag_key === tag.tag_key &&
|
||||||
selectedTag.tag_value === tag.tag_value
|
selectedTag.tag_value === tag.tag_value
|
||||||
) &&
|
)
|
||||||
(tag.tag_key.includes(filterValue) ||
|
|
||||||
tag.tag_value.includes(filterValue))
|
|
||||||
)
|
)
|
||||||
.slice(0, 12)
|
.slice(0, 12)
|
||||||
.map((tag) => (
|
.map((tag) => (
|
||||||
|
@ -2,6 +2,8 @@ import { containsObject, objectsAreEquivalent } from "@/lib/contains";
|
|||||||
import { Tag } from "@/lib/types";
|
import { Tag } from "@/lib/types";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { FiTag, FiX } from "react-icons/fi";
|
import { FiTag, FiX } from "react-icons/fi";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { getValidTags } from "@/lib/tags/tagUtils";
|
||||||
|
|
||||||
export function TagFilter({
|
export function TagFilter({
|
||||||
tags,
|
tags,
|
||||||
@ -14,6 +16,7 @@ export function TagFilter({
|
|||||||
}) {
|
}) {
|
||||||
const [filterValue, setFilterValue] = useState("");
|
const [filterValue, setFilterValue] = useState("");
|
||||||
const [tagOptionsAreVisible, setTagOptionsAreVisible] = useState(false);
|
const [tagOptionsAreVisible, setTagOptionsAreVisible] = useState(false);
|
||||||
|
const [filteredTags, setFilteredTags] = useState<Tag[]>(tags);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const popupRef = useRef<HTMLDivElement>(null);
|
const popupRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@ -45,14 +48,28 @@ export function TagFilter({
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filterValueLower = filterValue.toLowerCase();
|
const debouncedFetchTags = useRef(
|
||||||
const filteredTags = filterValueLower
|
debounce(async (value: string) => {
|
||||||
? tags.filter(
|
if (value) {
|
||||||
(tags) =>
|
const fetchedTags = await getValidTags(value);
|
||||||
tags.tag_value.toLowerCase().startsWith(filterValueLower) ||
|
setFilteredTags(fetchedTags);
|
||||||
tags.tag_key.toLowerCase().startsWith(filterValueLower)
|
} else {
|
||||||
)
|
setFilteredTags(tags);
|
||||||
: tags;
|
}
|
||||||
|
}, 50)
|
||||||
|
).current;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedFetchTags(filterValue);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedFetchTags.cancel();
|
||||||
|
};
|
||||||
|
}, [filterValue, tags, debouncedFetchTags]);
|
||||||
|
|
||||||
|
const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setFilterValue(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
@ -61,7 +78,7 @@ export function TagFilter({
|
|||||||
className="w-full border border-border py-0.5 px-2 rounded text-sm h-8"
|
className="w-full border border-border py-0.5 px-2 rounded text-sm h-8"
|
||||||
placeholder="Find a tag"
|
placeholder="Find a tag"
|
||||||
value={filterValue}
|
value={filterValue}
|
||||||
onChange={(event) => setFilterValue(event.target.value)}
|
onChange={handleFilterChange}
|
||||||
onFocus={() => setTagOptionsAreVisible(true)}
|
onFocus={() => setTagOptionsAreVisible(true)}
|
||||||
/>
|
/>
|
||||||
{selectedTags.length > 0 && (
|
{selectedTags.length > 0 && (
|
||||||
|
23
web/src/lib/tags/tagUtils.ts
Normal file
23
web/src/lib/tags/tagUtils.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Tag } from "../types";
|
||||||
|
|
||||||
|
export async function getValidTags(
|
||||||
|
matchPattern: string | null = null,
|
||||||
|
sources: string[] | null = null
|
||||||
|
): Promise<Tag[]> {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
if (matchPattern) params.append("match_pattern", matchPattern);
|
||||||
|
if (sources) sources.forEach((source) => params.append("sources", source));
|
||||||
|
|
||||||
|
const response = await fetch(`/api/query/valid-tags?${params.toString()}`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to fetch valid tags");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()).tags;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user