Skip to main content
ArcHive provides a comprehensive search system that helps you find exactly what you need from your personal knowledge base. Search across all your content using MongoDB’s text search index:
if (q) {
  const searchRegex = new RegExp(q, "i");
  findCriteria.$or = [
    { title: searchRegex },
    { description: searchRegex },
    { content: searchRegex },
    { url: searchRegex },
    { tags: searchRegex },
  ];
}
Source: backend/src/services/content.service.ts:138-147
Search is case-insensitive and matches partial words, so searching for “java” will find “JavaScript”, “java”, and “Java”.

Search Index Weights

The search index uses weighted fields to rank results:
ContentItemSchema.index(
  {
    title: "text",
    description: "text",
    content: "text",
    url: "text",
    tags: "text",
  },
  {
    name: "ContentItemTextIndex",
    weights: {
      title: 10,       // Highest priority
      tags: 5,         // Second priority
      description: 3,  // Third priority
      content: 1,      // Lower priority
      url: 1,          // Lower priority
    },
  },
);
Source: backend/src/db/models/ContentItem.ts:80-98
Matches in titles are weighted 10x higher than matches in content, so items with your search term in the title will appear first.

Search API Endpoint

The search endpoint supports multiple query parameters:
contentRoutes.get(
  "/",
  searchRateLimiter,
  validate("query", searchContentQuerySchema),
  async (c) => {
    const userId = c.get("user")?._id;
    const queryParams = c.req.valid("query") as SearchContentQuery;

    const result = await getContents(userId, queryParams);
    return c.json(
      {
        message: "Content items retrieved successfully",
        data: result.contents.map((item) => item.toObject()),
        meta: {
          totalCount: result.totalCount,
          page: result.page,
          limit: result.limit,
          totalPages: result.totalPages,
        },
      },
      200,
    );
  },
);
Source: backend/src/routes/content.ts:110-140

Query Parameters

q
string
Search term to match against title, description, content, URL, and tags
type
enum
Filter by content type: link, text, or code
tag
string
Filter by specific tag (stemmed for consistency)
platform
string
Filter by platform (e.g., github, youtube)
limit
number
default:"20"
Number of results per page (1-100)
page
number
default:"1"
Page number for pagination

Example Requests

GET /api/content?q=javascript

Mobile App Search Interface

Search UI Flow

The mobile app provides an intuitive search experience:
1

Activate Search

Tap the search icon in the header:
const toggleSearch = () => {
  setIsSearchVisible(!isSearchVisible);
  setSearchQuery("");
  headerHeight.value = withTiming(isSearchVisible ? 100 : 120, {
    duration: 300,
    easing: Easing.inOut(Easing.ease),
  });
};
Source: archive/app/(tabs)/index.tsx:92-99
2

Enter Search Query

Type in the search box with auto-focus and 300ms debounce:
const useDebounce = (value: string, delay: number) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
};
Source: archive/app/(tabs)/index.tsx:24-38
3

View Results

Results update automatically as you type, with infinite scroll for pagination
4

Apply Filters

Use type filters (All, Link, Text, Code) to narrow results

Search Query Hook

const {
  data,
  fetchNextPage,
  hasNextPage,
  isFetchingNextPage,
  refetch,
  isRefetching,
} = useInfiniteQuery({
  queryKey: ["contents", debouncedSearchQuery, selectedContentType],
  queryFn: async ({ pageParam = 1 }) => {
    if (debouncedSearchQuery) {
      await addRecentSearch(debouncedSearchQuery);
    }
    const response = await getContent(
      debouncedSearchQuery,
      pageParam,
      10,
      selectedContentType === "All" ? undefined : selectedContentType.toLowerCase(),
    );
    return response;
  },
  getNextPageParam: (lastPage) => {
    if (lastPage.meta.page < lastPage.meta.totalPages) {
      return lastPage.meta.page + 1;
    }
    return undefined;
  },
  initialPageParam: 1,
});
Source: archive/app/(tabs)/index.tsx:52-82

Recent Search History

ArcHive tracks your recent searches for quick access:

Storing Recent Searches

if (debouncedSearchQuery) {
  await addRecentSearch(debouncedSearchQuery);
}
Source: archive/app/(tabs)/index.tsx:63-64

Displaying Recent Searches

When the search box is active but empty, recent searches appear:
useEffect(() => {
  if (isSearchVisible && !debouncedSearchQuery) {
    getRecentSearches().then(setRecentSearches);
  }
}, [isSearchVisible, debouncedSearchQuery]);
Source: archive/app/(tabs)/index.tsx:86-90

Recent Search UI

{isSearchVisible && recentSearches.length > 0 ? (
  <View style={styles.recentSearchesContainer}>
    <View style={styles.recentSearchesHeader}>
      <FontAwesome5 name="history" size={20} color={colors.primary} />
      <Text style={[styles.recentSearchesTitle, { color: colors.text }]}>
        Recent Searches
      </Text>
    </View>
    <View style={styles.recentSearchesList}>
      {recentSearches.map((term) => (
        <TouchableOpacity
          key={term}
          onPress={() => setSearchQuery(term)}
          style={[styles.recentSearchItem, { backgroundColor: colors.card }]}
        >
          <FontAwesome5 name="search" size={14} color={colors.secondary} />
          <Text style={[styles.recentSearchText, { color: colors.text }]}>
            {term}
          </Text>
          <FontAwesome5 name="arrow-right" size={14} color={colors.secondary} />
        </TouchableOpacity>
      ))}
    </View>
  </View>
)}
Source: archive/app/(tabs)/index.tsx:223-263
Tapping a recent search term instantly performs that search again, saving you time on repeated queries.

Content Type Filtering

Visual filter buttons appear below the search bar:
const contentTypes = ["All", "Link", "Text", "Code"];

<View style={styles.filterContainer}>
  {contentTypes.map((type) => {
    const isSelected = selectedContentType === type;
    const getIcon = () => {
      switch (type) {
        case "Link": return "link";
        case "Text": return "file-text";
        case "Code": return "code";
        default: return "th-large";
      }
    };

    return (
      <TouchableOpacity
        key={type}
        style={[
          styles.filterButton,
          isSelected && {
            backgroundColor: colors.primary,
            borderColor: colors.primary,
            shadowColor: colors.primary,
          },
        ]}
        onPress={() => setSelectedContentType(type)}
      >
        <FontAwesome5
          name={getIcon()}
          size={14}
          color={isSelected ? "#FFFFFF" : colors.secondary}
        />
        <Text style={[styles.filterButtonText]}>
          {type}
        </Text>
      </TouchableOpacity>
    );
  })}
</View>
Source: archive/app/(tabs)/index.tsx:144-196 Search by tags using Porter Stemming for consistency:
if (tag) {
  const stemmedTag = natural.PorterStemmer.stem(tag.toLowerCase());
  findCriteria.tags = { $in: [stemmedTag] };
}
Source: backend/src/services/content.service.ts:153-156
Tag search uses stemming, so searching for “running” will also find items tagged with “run” or “runner”.

Search Rate Limiting

Search requests are rate-limited separately from other API calls using the searchRateLimiter middleware to ensure fair usage and system stability.
Source: backend/src/routes/content.ts:112

Empty States

ArcHive provides contextual empty states for different scenarios:

Search Empty State

emptyStateType={
  debouncedSearchQuery
    ? "search"
    : selectedContentType !== "All"
      ? "filter"
      : "default"
}
Source: archive/app/(tabs)/index.tsx:204-210
  • search: When search query returns no results
  • filter: When type filter has no matching content
  • default: When you haven’t saved any content yet

Search Tips UI

When search is active but no query entered:
<View style={styles.emptySearchContainer}>
  <View style={[styles.emptySearchIconContainer]}>
    <FontAwesome5 name="search" size={56} color={colors.primary} />
  </View>
  <Text style={[styles.emptySearchTitle, { color: colors.text }]}>
    Start Your Search
  </Text>
  <Text style={[styles.emptySearchDescription, { color: colors.secondary }]}>
    Type in the search box above to find your saved links, notes, and code snippets
  </Text>
  <View style={styles.searchTipsContainer}>
    <View style={styles.searchTipItem}>
      <View style={[styles.tipBadge]}>
        <FontAwesome5 name="lightbulb" size={14} color={colors.primary} />
      </View>
      <Text style={[styles.searchTipText, { color: colors.secondary }]}>
        Search by title, description, or content
      </Text>
    </View>
    <View style={styles.searchTipItem}>
      <View style={[styles.tipBadge]}>
        <FontAwesome5 name="filter" size={14} color={colors.primary} />
      </View>
      <Text style={[styles.searchTipText, { color: colors.secondary }]}>
        Use filters to narrow down by type
      </Text>
    </View>
  </View>
</View>
Source: archive/app/(tabs)/index.tsx:265-326

Performance Optimization

Search queries are debounced to reduce API calls:
const debouncedSearchQuery = useDebounce(searchQuery, 300);
Source: archive/app/(tabs)/index.tsx:46 This means the search only executes 300ms after you stop typing, reducing server load and improving responsiveness.

Pagination

Results are paginated for better performance:
const result = await getContents(userId, queryParams);

return {
  contents,
  totalCount,
  page,
  limit,
  totalPages: Math.ceil(totalCount / limit),
};
Source: backend/src/services/content.service.ts:174-180

Infinite Scroll

The mobile app loads more results as you scroll:
onEndReached={() => {
  if (hasNextPage && !isFetchingNextPage) {
    fetchNextPage();
  }
}}
Source: archive/app/(tabs)/index.tsx:212-216

Next Steps

Organization

Learn about content organization

Profile Management

Manage your profile and view statistics

Build docs developers (and LLMs) love