All files useSearch.ts

96.87% Statements 31/32
84.21% Branches 16/19
100% Functions 11/11
100% Lines 29/29

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147            2x 2x                             2x 2x 2x     2x   2x                                                 2x 608x             52x                   2x 2x       41x   39x   41x             77x 3333x                       41x 41x   41x   41x   41x 5160x             460x                   41x   41x         1x 1x     37x                  
import MiniSearch from "minisearch";
import { ref, shallowRef } from "vue";
import type { Template } from "../types/template";
import type { Stack } from "../types/stack";
 
// Singleton state (module-level)
let searchIndex: MiniSearch | null = null;
let isIndexed = false;
 
export type SearchableItem = (Template | Stack) & {
  itemType: "template" | "stack";
  id: string;
  tags?: string[];
  category?: string;
};
 
export interface SearchResult {
  item: SearchableItem;
  score: number;
  match: Record<string, string[]>;
}
 
const query = ref("");
const results = shallowRef<SearchResult[]>([]);
const isSearching = ref(false);
 
function initializeIndex(templates: Template[], stacks: Stack[]) {
  Iif (isIndexed) return;
 
  searchIndex = new MiniSearch({
    fields: ["name", "description", "tags", "category", "type"],
    storeFields: [
      "name",
      "type",
      "description",
      "icon",
      "category",
      "itemType",
    ],
    searchOptions: {
      boost: {
        name: 3, // Highest priority for name matches
        tags: 2, // Stack tags, template keywords
        category: 1.5, // Type categorization
        type: 1.5, // Template type
        description: 1, // Base priority
      },
      fuzzy: 0.2,
      prefix: true,
      combineWith: "AND",
    },
  });
 
  // Combine templates + stacks with unique IDs
  const searchableItems: SearchableItem[] = [
    ...templates.map((t, i) => ({
      ...t,
      id: `template-${i}`,
      itemType: "template" as const,
      tags: (t as any).tags || [],
      category: t.type,
    })),
    ...stacks.map((s, i) => ({
      ...s,
      id: `stack-${i}`,
      itemType: "stack" as const,
      type: "stack",
      tags: s.tags || [],
      category: "stack",
    })),
  ];
 
  searchIndex.addAll(searchableItems);
  isIndexed = true;
}
 
function search(q: string, allItems: SearchableItem[]): SearchResult[] {
  if (!q || q.length < 2) return [];
 
  const fuzzy = q.length <= 3 ? 0.3 : 0.2;
 
  return searchIndex!
    .search(q, {
      fuzzy,
      prefix: true,
      combineWith: "AND",
    })
    .slice(0, 8)
    .map((r) => ({
      item: allItems.find((item) => item.id === r.id)!,
      score: r.score,
      match: r.match,
    }));
}
 
export function useSearch() {
  function performSearch(
    q: string,
    templates: Template[],
    stacks: Stack[] = [],
  ) {
    query.value = q;
    isSearching.value = true;
 
    try {
      // Initialize index with both templates and stacks
      if (!isIndexed) initializeIndex(templates, stacks);
 
      const allItems: SearchableItem[] = [
        ...templates.map((t, i) => ({
          ...t,
          id: `template-${i}`,
          itemType: "template" as const,
          tags: (t as any).tags || [],
          category: t.type,
        })),
        ...stacks.map((s, i) => ({
          ...s,
          id: `stack-${i}`,
          itemType: "stack" as const,
          type: "stack",
          tags: s.tags || [],
          category: "stack",
        })),
      ];
 
      results.value = search(q, allItems);
    } finally {
      isSearching.value = false;
    }
  }
 
  function clearSearch() {
    query.value = "";
    results.value = [];
  }
 
  return {
    query,
    results,
    isSearching,
    performSearch,
    clearSearch,
    initializeIndex,
  };
}