Compare commits

...

2 Commits

Author SHA1 Message Date
Onyx Trialee 2
8cdf9a5d4f mend 2025-10-30 18:43:13 -07:00
Onyx Trialee 2
667d16c1f6 Filter documents in Document Explorer page by connector 2025-10-30 17:58:39 -07:00
8 changed files with 107 additions and 4 deletions

View File

@@ -118,6 +118,7 @@ class BaseFilters(BaseModel):
kg_terms: list[str] | None = None
kg_sources: list[str] | None = None
kg_chunk_id_zero_only: bool | None = False
connector_ids: list[int] | None = None
class UserFileFilters(BaseModel):

View File

@@ -218,6 +218,17 @@ def get_documents_for_connector_credential_pair(
return db_session.scalars(stmt).all()
def get_document_ids_for_connector_ids(
db_session: Session, connector_ids: list[int]
) -> set[str]:
if not connector_ids:
return set()
stmt = select(DocumentByConnectorCredentialPair.id).where(
DocumentByConnectorCredentialPair.connector_id.in_(connector_ids)
)
return set(db_session.scalars(stmt).all())
def get_documents_by_ids(
db_session: Session,
document_ids: list[str],

View File

@@ -21,6 +21,7 @@ from onyx.db.chat import get_search_docs_for_chat_message
from onyx.db.chat import get_valid_messages_from_query_sessions
from onyx.db.chat import translate_db_message_to_chat_message_detail
from onyx.db.chat import translate_db_search_doc_to_server_search_doc
from onyx.db.document import get_document_ids_for_connector_ids
from onyx.db.engine.sql_engine import get_session
from onyx.db.models import User
from onyx.db.search_settings import get_current_search_settings
@@ -80,6 +81,12 @@ def admin_search(
documents = SearchDoc.from_chunks_or_sections(matching_chunks)
if getattr(question.filters, "connector_ids", None):
allowed_doc_ids = get_document_ids_for_connector_ids(
db_session=db_session, connector_ids=question.filters.connector_ids or []
)
documents = [d for d in documents if d.document_id in allowed_doc_ids]
# Deduplicate documents by id
deduplicated_documents: list[SearchDoc] = []
seen_documents: set[str] = set()

View File

@@ -14,7 +14,7 @@ import { useRouter } from "next/navigation";
import { useFilters } from "@/lib/hooks";
import { buildFilters } from "@/lib/search/utils";
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
import { DocumentSetSummary } from "@/lib/types";
import { DocumentSetSummary, ValidSources } from "@/lib/types";
import { SourceIcon } from "@/components/SourceIcon";
import { Connector } from "@/lib/connectors/connectors";
import { HorizontalFilters } from "@/components/filters/SourceSelector";
@@ -124,6 +124,9 @@ export function Explorer({
const [isLoading, setIsLoading] = useState(false);
const filterManager = useFilters();
const [selectedConnectorIds, setSelectedConnectorIds] = useState<number[]>(
[]
);
const onSearch = useCallback(
async (query: string) => {
@@ -133,7 +136,8 @@ export function Explorer({
filterManager.selectedSources,
filterManager.selectedDocumentSets,
filterManager.timeRange,
filterManager.selectedTags
filterManager.selectedTags,
selectedConnectorIds.length ? selectedConnectorIds : null
);
const results = await adminSearch(query, filters);
if (results.ok) {
@@ -149,6 +153,7 @@ export function Explorer({
filterManager.selectedSources,
filterManager.timeRange,
filterManager.selectedTags,
selectedConnectorIds,
]
);
@@ -167,6 +172,7 @@ export function Explorer({
filterManager.selectedDocumentSets,
filterManager.selectedSources,
filterManager.timeRange,
selectedConnectorIds,
]);
return (
@@ -197,6 +203,16 @@ export function Explorer({
availableDocumentSets={documentSets}
existingSources={connectors.map((connector) => connector.source)}
availableTags={[]}
availableConnectors={connectors.map((c) => ({
id: c.id,
name:
c.source === (ValidSources as any).File &&
(c as any)?.connector_specific_config?.file_names?.[0]
? (c as any).connector_specific_config.file_names[0]
: (c as any).displayName ?? c.name,
}))}
selectedConnectorIds={selectedConnectorIds}
setSelectedConnectorIds={setSelectedConnectorIds}
toggleFilters={() => {}}
filtersUntoggled={false}
tagsOnLeft={true}

View File

@@ -40,6 +40,9 @@ export interface SourceSelectorProps {
toggleFilters: () => void;
filtersUntoggled: boolean;
tagsOnLeft: boolean;
availableConnectors?: { id: number; name: string }[];
selectedConnectorIds?: number[];
setSelectedConnectorIds?: React.Dispatch<React.SetStateAction<number[]>>;
}
export function SelectedBubble({
@@ -72,6 +75,9 @@ export function HorizontalFilters({
setSelectedDocumentSets,
availableDocumentSets,
existingSources,
availableConnectors,
selectedConnectorIds,
setSelectedConnectorIds,
}: SourceSelectorProps) {
const handleSourceSelect = (source: SourceMetadata) => {
setSelectedSources((prev: SourceMetadata[]) => {
@@ -98,6 +104,9 @@ export function HorizontalFilters({
const availableSources = allSources.filter((source) =>
existingSources.includes(source.internalName)
);
const connectorNameById = new Map(
(availableConnectors || []).map((c) => [c.id, c.name])
);
return (
<div className="b">
@@ -138,6 +147,32 @@ export function HorizontalFilters({
}
defaultDisplay="All Sources"
/>
{availableConnectors && availableConnectors.length > 0 && (
<FilterDropdown
width="w-52"
options={availableConnectors.map((c) => ({
key: String(c.id),
display: <span className="ml-2 text-sm">{c.name}</span>,
}))}
selected={(selectedConnectorIds || []).map(String)}
selectedDisplay={(selectedConnectorIds || []).map(
(id) => connectorNameById.get(id) || String(id)
)}
handleSelect={(option) => {
if (!setSelectedConnectorIds) return;
const id = Number(option.key);
setSelectedConnectorIds((prev: number[]) =>
prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]
);
}}
icon={
<div className="my-auto mr-2 w-[16px] h-[16px]">
<FiFilter size={16} />
</div>
}
defaultDisplay="All Connectors"
/>
)}
{availableDocumentSets.length > 0 && (
<FilterDropdown
width="w-52"
@@ -202,6 +237,28 @@ export function HorizontalFilters({
</>
</SelectedBubble>
))}
{selectedConnectorIds &&
selectedConnectorIds.length > 0 &&
selectedConnectorIds.map((id) => (
<SelectedBubble
key={id}
onClick={() =>
setSelectedConnectorIds &&
setSelectedConnectorIds((prev: number[]) =>
prev.filter((x) => x !== id)
)
}
>
<>
<div>
<FiFilter />
</div>
<span className="ml-2 text-sm truncate max-w-40">
{connectorNameById.get(id) || `Connector ${id}`}
</span>
</>
</SelectedBubble>
))}
</div>
</div>
</div>

View File

@@ -10,6 +10,7 @@ interface Option {
export function FilterDropdown({
options,
selected,
selectedDisplay,
handleSelect,
icon,
defaultDisplay,
@@ -22,6 +23,7 @@ export function FilterDropdown({
}: {
options: Option[];
selected: string[];
selectedDisplay?: string[];
handleSelect: (option: Option) => void;
icon: JSX.Element;
defaultDisplay: string | JSX.Element;
@@ -114,7 +116,12 @@ export function FilterDropdown({
{selected.length === 0 || resetValues ? (
defaultDisplay
) : (
<p className="line-clamp-1">{selected.join(", ")}</p>
<p className="line-clamp-1">
{(selectedDisplay && selectedDisplay.length > 0
? selectedDisplay
: selected
).join(", ")}
</p>
)}
{resetValues && selected.length !== 0 ? (
<div

View File

@@ -153,6 +153,8 @@ export interface Filters {
source_type: string[] | null;
document_set: string[] | null;
time_cutoff: Date | null;
tags?: Tag[];
connector_ids?: number[] | null;
}
export interface SearchRequestArgs {

View File

@@ -6,7 +6,8 @@ export const buildFilters = (
sources: SourceMetadata[],
documentSets: string[],
timeRange: DateRangePickerValue | null,
tags: Tag[]
tags: Tag[],
connectorIds?: number[] | null
): Filters => {
const filters = {
source_type:
@@ -14,6 +15,7 @@ export const buildFilters = (
document_set: documentSets.length > 0 ? documentSets : null,
time_cutoff: timeRange?.from ? timeRange.from : null,
tags: tags,
connector_ids: connectorIds ?? null,
};
return filters;