search-web-api 1.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/README.md +307 -0
  2. package/demo/index.ts +38 -0
  3. package/demo/openapi.ts +402 -0
  4. package/demo/routes/autocomplete.ts +124 -0
  5. package/demo/routes/search.ts +61 -0
  6. package/docs/AUTOCOMPLETE.md +296 -0
  7. package/docs/CATEGORY_SEARCH.md +265 -0
  8. package/package.json +32 -0
  9. package/src/autocomplete/autocomplete-ai-next-word-predictor.ts +47 -0
  10. package/src/autocomplete/autocomplete-search-engine-backends.ts +231 -0
  11. package/src/category-registry.ts +6 -0
  12. package/src/config/search-engine-constants.ts +216 -0
  13. package/src/constants.ts +6 -0
  14. package/src/engine-descriptions.ts +7 -0
  15. package/src/engine-status.ts +7 -0
  16. package/src/engine.ts +6 -0
  17. package/src/registry/search-engine-category-registry.ts +200 -0
  18. package/src/registry/search-engine-descriptions.ts +121 -0
  19. package/src/registry/search-engine-status-tracker.ts +240 -0
  20. package/src/result-container.ts +8 -0
  21. package/src/search/search-engines-registry-list.ts +205 -0
  22. package/src/search/search-query-executor.ts +168 -0
  23. package/src/search/search-result-container.ts +421 -0
  24. package/src/search-web-types.ts +7 -0
  25. package/src/search.ts +10 -0
  26. package/src/sources/academic/arxiv.ts +72 -0
  27. package/src/sources/academic/core.ts +65 -0
  28. package/src/sources/academic/crossref.ts +75 -0
  29. package/src/sources/academic/doaj.ts +63 -0
  30. package/src/sources/academic/google_scholar.ts +53 -0
  31. package/src/sources/academic/openalex.ts +75 -0
  32. package/src/sources/academic/pubmed.ts +96 -0
  33. package/src/sources/academic/semantic_scholar.ts +77 -0
  34. package/src/sources/academic/wikidata.ts +44 -0
  35. package/src/sources/general/baidu.ts +59 -0
  36. package/src/sources/general/bing.ts +30 -0
  37. package/src/sources/general/brave.ts +54 -0
  38. package/src/sources/general/duckduckgo.ts +49 -0
  39. package/src/sources/general/google.ts +68 -0
  40. package/src/sources/general/mojeek.ts +56 -0
  41. package/src/sources/general/qwant.ts +37 -0
  42. package/src/sources/general/startpage.ts +44 -0
  43. package/src/sources/general/yahoo.ts +41 -0
  44. package/src/sources/general/yandex.ts +56 -0
  45. package/src/sources/images/bing_images.ts +68 -0
  46. package/src/sources/images/deviantart.ts +71 -0
  47. package/src/sources/images/flickr.ts +132 -0
  48. package/src/sources/images/google_images.ts +101 -0
  49. package/src/sources/images/imgur.ts +58 -0
  50. package/src/sources/images/openclipart.ts +52 -0
  51. package/src/sources/images/pixabay.ts +56 -0
  52. package/src/sources/images/unsplash.ts +38 -0
  53. package/src/sources/images/wallhaven.ts +50 -0
  54. package/src/sources/it/crates.ts +41 -0
  55. package/src/sources/it/dockerhub.ts +47 -0
  56. package/src/sources/it/github.ts +37 -0
  57. package/src/sources/it/gitlab.ts +55 -0
  58. package/src/sources/it/npm.ts +36 -0
  59. package/src/sources/it/packagist.ts +43 -0
  60. package/src/sources/it/pypi.ts +30 -0
  61. package/src/sources/it/rubygems.ts +43 -0
  62. package/src/sources/it/stackoverflow.ts +39 -0
  63. package/src/sources/maps/apple_maps.ts +105 -0
  64. package/src/sources/maps/openstreetmap.ts +34 -0
  65. package/src/sources/maps/photon.ts +77 -0
  66. package/src/sources/news/bing_news.ts +95 -0
  67. package/src/sources/news/google_news.ts +80 -0
  68. package/src/sources/news/hackernews.ts +94 -0
  69. package/src/sources/news/yahoo_news.ts +77 -0
  70. package/src/sources/shopping/ebay.ts +96 -0
  71. package/src/sources/social/mastodon.ts +46 -0
  72. package/src/sources/social/medium.ts +52 -0
  73. package/src/sources/social/reddit.ts +48 -0
  74. package/src/sources/social/soundcloud.ts +64 -0
  75. package/src/sources/social/twitter.ts +56 -0
  76. package/src/sources/specialized/annas_archive.ts +97 -0
  77. package/src/sources/specialized/archive.ts +48 -0
  78. package/src/sources/specialized/genius.ts +43 -0
  79. package/src/sources/specialized/goodreads.ts +62 -0
  80. package/src/sources/specialized/imdb.ts +55 -0
  81. package/src/sources/specialized/openlibrary.ts +59 -0
  82. package/src/sources/specialized/wikipedia.ts +37 -0
  83. package/src/sources/specialized/wttr.ts +98 -0
  84. package/src/sources/torrents/1337x.ts +43 -0
  85. package/src/sources/torrents/eztv.ts +47 -0
  86. package/src/sources/torrents/kickass.ts +63 -0
  87. package/src/sources/torrents/nyaa.ts +53 -0
  88. package/src/sources/torrents/solidtorrents.ts +68 -0
  89. package/src/sources/torrents/thepiratebay.ts +57 -0
  90. package/src/sources/torrents/yts.ts +54 -0
  91. package/src/sources/videos/bing_videos.ts +91 -0
  92. package/src/sources/videos/dailymotion.ts +101 -0
  93. package/src/sources/videos/invidious.ts +86 -0
  94. package/src/sources/videos/peertube.ts +76 -0
  95. package/src/sources/videos/vimeo.ts +67 -0
  96. package/src/sources/videos/youtube.ts +78 -0
  97. package/src/suggest-next-words/autocomplete-ai.ts +38 -0
  98. package/src/suggest-next-words/autocomplete-search-engines.ts +384 -0
  99. package/src/suggest-next-words/misspelled-typos-8k.json +1 -0
  100. package/src/types/search-engine-interface.ts +27 -0
  101. package/src/types/search-result-types.ts +405 -0
  102. package/test/api.test.ts +128 -0
  103. package/test/autocomplete-ai.test.ts +20 -0
  104. package/test/autocomplete-engines.test.ts +131 -0
  105. package/test/engine-health-suite.test.ts +350 -0
  106. package/test/search.test.ts +69 -0
  107. package/test/sources-unit.test.ts +1152 -0
  108. package/test/sources.test.ts +182 -0
  109. package/test/test-utils.ts +81 -0
  110. package/tsconfig.json +16 -0
  111. package/vitest.config.ts +20 -0
@@ -0,0 +1,65 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const core: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const apiKey = process.env.CORE_API_KEY;
8
+ if (!apiKey) return [];
9
+
10
+ const limit = 10;
11
+ const offset = ((page || 1) - 1) * limit;
12
+
13
+ const response = await fetch(
14
+ "https://api.core.ac.uk/v3/search/works",
15
+ {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ Authorization: `Bearer ${apiKey}`,
20
+ "User-Agent":
21
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
22
+ },
23
+ body: JSON.stringify({ q: query, limit, offset, scroll: false }),
24
+ }
25
+ );
26
+
27
+ if (!response.ok) return [];
28
+
29
+ const data = await response.json();
30
+
31
+ if (!data || !Array.isArray(data.results)) return [];
32
+
33
+ return data.results
34
+ .map((r: any) => {
35
+ const title = r.title || "";
36
+ const doi = r.doi || "";
37
+ const url =
38
+ r.downloadUrl ||
39
+ (doi ? `https://doi.org/${doi}` : "") ||
40
+ (r.id ? `https://core.ac.uk/works/${r.id}` : "");
41
+ const authors = (r.authors || [])
42
+ .slice(0, 3)
43
+ .map((a: any) => a.name)
44
+ .filter(Boolean);
45
+ const journal = r.journals?.[0]?.title || "";
46
+ const abstract = (r.abstract || "").slice(0, 500);
47
+
48
+ const content = [
49
+ abstract,
50
+ authors.length
51
+ ? `Authors: ${authors.join(", ")}${
52
+ (r.authors || []).length > 3 ? " et al." : ""
53
+ }`
54
+ : "",
55
+ r.yearPublished ? `Year: ${r.yearPublished}` : "",
56
+ journal ? `Journal: ${journal}` : "",
57
+ doi ? `DOI: ${doi}` : "",
58
+ ]
59
+ .filter(Boolean)
60
+ .join("\n");
61
+
62
+ return { url, title, content, engine: "core" };
63
+ })
64
+ .filter((r: any) => r.url && r.title);
65
+ };
@@ -0,0 +1,75 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const crossref: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ query: query,
9
+ offset: String(20 * ((page || 1) - 1)),
10
+ });
11
+
12
+ const response = await fetch(
13
+ `https://api.crossref.org/works?${params}`,
14
+ {
15
+ headers: {
16
+ "User-Agent":
17
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
18
+ },
19
+ }
20
+ );
21
+
22
+ if (!response.ok) return [];
23
+
24
+ const data = await response.json();
25
+
26
+ if (!data || !data.message || !data.message.items) return [];
27
+
28
+ return data.message.items
29
+ .filter((record: any) => record.type !== "component")
30
+ .map((record: any) => {
31
+ let title = "";
32
+ let journal = "";
33
+
34
+ if (record.type === "book-chapter") {
35
+ title = record["container-title"]?.[0] || "";
36
+ if (
37
+ record.title?.[0] &&
38
+ record.title[0].toLowerCase().trim() !==
39
+ title.toLowerCase().trim()
40
+ ) {
41
+ title += ` (${record.title[0]})`;
42
+ }
43
+ } else {
44
+ title =
45
+ record.title?.[0] || record["container-title"]?.[0] || "";
46
+ journal =
47
+ record["container-title"]?.[0] && record.title?.[0]
48
+ ? record["container-title"][0]
49
+ : "";
50
+ }
51
+
52
+ const authors = (record.author || [])
53
+ .map((a: any) => `${a.given || ""} ${a.family || ""}`.trim())
54
+ .filter((a: string) => a)
55
+ .join(", ");
56
+
57
+ const content = [
58
+ record.abstract || "",
59
+ journal ? `Journal: ${journal}` : "",
60
+ authors ? `Authors: ${authors}` : "",
61
+ record.publisher ? `Publisher: ${record.publisher}` : "",
62
+ record.DOI ? `DOI: ${record.DOI}` : "",
63
+ ]
64
+ .filter(Boolean)
65
+ .join("\n");
66
+
67
+ return {
68
+ url: record.URL || `https://doi.org/${record.DOI}` || "",
69
+ title,
70
+ content,
71
+ engine: "crossref",
72
+ };
73
+ })
74
+ .filter((r: any) => r.url && r.title);
75
+ };
@@ -0,0 +1,63 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const doaj: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ pageSize: "10",
9
+ page: String(page || 1),
10
+ sort: "created_date:desc",
11
+ });
12
+
13
+ const response = await fetch(
14
+ `https://doaj.org/api/v2/search/articles/${encodeURIComponent(query)}?${params}`,
15
+ {
16
+ headers: {
17
+ "User-Agent":
18
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
19
+ Accept: "application/json",
20
+ },
21
+ }
22
+ );
23
+
24
+ if (!response.ok) return [];
25
+
26
+ const data = await response.json();
27
+
28
+ if (!data || !Array.isArray(data.results)) return [];
29
+
30
+ return data.results
31
+ .map((r: any) => {
32
+ const bib = r.bibjson || {};
33
+ const title = bib.title || "";
34
+ const doi =
35
+ bib.identifier?.find((i: any) => i.type === "doi")?.id || "";
36
+ const link =
37
+ bib.link?.find((l: any) => l.type === "fulltext")?.url || "";
38
+ const url = link || (doi ? `https://doi.org/${doi}` : "");
39
+ const authors = (bib.author || [])
40
+ .slice(0, 3)
41
+ .map((a: any) => a.name)
42
+ .filter(Boolean);
43
+ const journal = bib.journal?.title || "";
44
+ const abstract = (bib.abstract || "").slice(0, 500);
45
+
46
+ const content = [
47
+ abstract,
48
+ authors.length
49
+ ? `Authors: ${authors.join(", ")}${
50
+ (bib.author || []).length > 3 ? " et al." : ""
51
+ }`
52
+ : "",
53
+ bib.year ? `Year: ${bib.year}` : "",
54
+ journal ? `Journal: ${journal}` : "",
55
+ doi ? `DOI: ${doi}` : "",
56
+ ]
57
+ .filter(Boolean)
58
+ .join("\n");
59
+
60
+ return { url, title, content, engine: "doaj" };
61
+ })
62
+ .filter((r: any) => r.url && r.title);
63
+ };
@@ -0,0 +1,53 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction } from "../../types/search-engine-interface.js";
3
+
4
+ export const google_scholar: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ q: query,
10
+ start: String(((page || 1) - 1) * 10),
11
+ hl: "en",
12
+ });
13
+
14
+ const response = await fetch(
15
+ `https://scholar.google.com/scholar?${params}`,
16
+ {
17
+ headers: {
18
+ "User-Agent":
19
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
20
+ Accept:
21
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
22
+ "Accept-Language": "en-US,en;q=0.9",
23
+ Cookie: "CONSENT=YES+",
24
+ },
25
+ }
26
+ );
27
+
28
+ if (!response.ok) return [];
29
+
30
+ const text = await response.text();
31
+ const { document } = parseHTML(text);
32
+
33
+ return Array.from(document.querySelectorAll(".gs_r.gs_or.gs_scl"))
34
+ .map((element) => {
35
+ const titleLink = element.querySelector(".gs_rt a");
36
+ const url = titleLink?.getAttribute("href") || "";
37
+ const title = titleLink?.textContent?.trim() || "";
38
+ const content =
39
+ element.querySelector(".gs_rs")?.textContent?.trim() || "";
40
+ const publicationInfo =
41
+ element.querySelector(".gs_a")?.textContent?.trim() || "";
42
+
43
+ return {
44
+ url,
45
+ title,
46
+ content: publicationInfo
47
+ ? `${publicationInfo} - ${content}`
48
+ : content,
49
+ engine: "google_scholar",
50
+ };
51
+ })
52
+ .filter((r) => r.url && r.title);
53
+ };
@@ -0,0 +1,75 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ function reconstructAbstract(
4
+ invertedIndex: Record<string, number[]>
5
+ ): string {
6
+ const words: string[] = [];
7
+ for (const [word, positions] of Object.entries(invertedIndex)) {
8
+ for (const pos of positions) words[pos] = word;
9
+ }
10
+ return words.filter(Boolean).join(" ");
11
+ }
12
+
13
+ export const openalex: EngineFunction = async (
14
+ query: string,
15
+ page: number | undefined
16
+ ) => {
17
+ const params = new URLSearchParams({
18
+ search: query,
19
+ "per-page": "10",
20
+ page: String(page || 1),
21
+ sort: "cited_by_count:desc",
22
+ mailto: process.env.OPENALEX_MAILTO || "research@qwksearch.com",
23
+ });
24
+
25
+ const response = await fetch(
26
+ `https://api.openalex.org/works?${params}`,
27
+ {
28
+ headers: {
29
+ "User-Agent":
30
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
31
+ Accept: "application/json",
32
+ },
33
+ }
34
+ );
35
+
36
+ if (!response.ok) return [];
37
+
38
+ const data = await response.json();
39
+
40
+ if (!data || !Array.isArray(data.results)) return [];
41
+
42
+ return data.results
43
+ .map((w: any) => {
44
+ const title = w.title || "";
45
+ const doi = w.doi || "";
46
+ const url =
47
+ w.primary_location?.landing_page_url || doi || w.id || "";
48
+ const authors = (w.authorships || [])
49
+ .slice(0, 3)
50
+ .map((a: any) => a.author?.display_name)
51
+ .filter(Boolean);
52
+ const abstract = w.abstract_inverted_index
53
+ ? reconstructAbstract(w.abstract_inverted_index).slice(0, 500)
54
+ : "";
55
+ const journal = w.host_venue?.display_name || "";
56
+
57
+ const content = [
58
+ abstract,
59
+ authors.length
60
+ ? `Authors: ${authors.join(", ")}${
61
+ (w.authorships || []).length > 3 ? " et al." : ""
62
+ }`
63
+ : "",
64
+ w.publication_year ? `Year: ${w.publication_year}` : "",
65
+ journal ? `Journal: ${journal}` : "",
66
+ w.cited_by_count ? `Citations: ${w.cited_by_count}` : "",
67
+ doi ? `DOI: ${doi}` : "",
68
+ ]
69
+ .filter(Boolean)
70
+ .join("\n");
71
+
72
+ return { url, title, content, engine: "openalex" };
73
+ })
74
+ .filter((r: any) => r.url && r.title);
75
+ };
@@ -0,0 +1,96 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction } from "../../types/search-engine-interface.js";
3
+
4
+ export const pubmed: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const pageno = page || 1;
9
+ const number_of_results = 10;
10
+ const retstart = (pageno - 1) * number_of_results;
11
+
12
+ const esearchParams = new URLSearchParams({
13
+ db: "pubmed",
14
+ term: query,
15
+ retstart: String(retstart),
16
+ retmax: String(number_of_results),
17
+ });
18
+
19
+ const esearchResponse = await fetch(
20
+ `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/esearch.fcgi?${esearchParams}`,
21
+ {
22
+ headers: {
23
+ "User-Agent":
24
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
25
+ },
26
+ }
27
+ );
28
+
29
+ if (!esearchResponse.ok) return [];
30
+
31
+ const esearchText = await esearchResponse.text();
32
+ const { document: esearchDoc } = parseHTML(esearchText);
33
+ const pmids: string[] = [];
34
+ esearchDoc.querySelectorAll("Id").forEach((elem) => {
35
+ const pmid = elem.textContent;
36
+ if (pmid) pmids.push(pmid);
37
+ });
38
+
39
+ if (pmids.length === 0) return [];
40
+
41
+ const efetchResponse = await fetch(
42
+ `https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=pubmed&retmode=xml&id=${pmids.join(",")}`,
43
+ {
44
+ headers: {
45
+ "User-Agent":
46
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
47
+ },
48
+ }
49
+ );
50
+
51
+ if (!efetchResponse.ok) return [];
52
+
53
+ const efetchText = await efetchResponse.text();
54
+ const { document } = parseHTML(efetchText);
55
+
56
+ return Array.from(document.querySelectorAll("PubmedArticle"))
57
+ .map((article) => {
58
+ const title =
59
+ article.querySelector("ArticleTitle")?.textContent || "";
60
+ const pmid = article.querySelector("PMID")?.textContent || "";
61
+ const url = `https://www.ncbi.nlm.nih.gov/pubmed/${pmid}`;
62
+
63
+ const abstractElements = article.querySelectorAll("AbstractText");
64
+ const abstract = Array.from(abstractElements)
65
+ .map((el) => el.textContent)
66
+ .join(" ");
67
+
68
+ const journal =
69
+ article.querySelector("Journal Title")?.textContent || "";
70
+ const doi =
71
+ article.querySelector('ELocationID[EIdType="doi"]')
72
+ ?.textContent || "";
73
+
74
+ const authors: string[] = [];
75
+ article.querySelectorAll("AuthorList Author").forEach((author) => {
76
+ const firstName =
77
+ author.querySelector("ForeName")?.textContent || "";
78
+ const lastName =
79
+ author.querySelector("LastName")?.textContent || "";
80
+ const authorName = `${firstName} ${lastName}`.trim();
81
+ if (authorName) authors.push(authorName);
82
+ });
83
+
84
+ const content = [
85
+ abstract,
86
+ journal ? `Journal: ${journal}` : "",
87
+ authors.length ? `Authors: ${authors.join(", ")}` : "",
88
+ doi ? `DOI: ${doi}` : "",
89
+ ]
90
+ .filter(Boolean)
91
+ .join("\n");
92
+
93
+ return { url, title, content, engine: "pubmed" };
94
+ })
95
+ .filter((r) => r.url && r.title);
96
+ };
@@ -0,0 +1,77 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const semantic_scholar: EngineFunction = async (
4
+ query: string,
5
+ page: number | undefined
6
+ ) => {
7
+ const response = await fetch(
8
+ "https://www.semanticscholar.org/api/1/search",
9
+ {
10
+ method: "POST",
11
+ headers: {
12
+ "User-Agent":
13
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
14
+ "Content-Type": "application/json",
15
+ },
16
+ body: JSON.stringify({
17
+ queryString: query,
18
+ page: page || 1,
19
+ pageSize: 10,
20
+ sort: "relevance",
21
+ getQuerySuggestions: false,
22
+ authors: [],
23
+ coAuthors: [],
24
+ venues: [],
25
+ performTitleMatch: true,
26
+ }),
27
+ }
28
+ );
29
+
30
+ if (!response.ok) return [];
31
+
32
+ const data = await response.json();
33
+
34
+ if (!data.results) return [];
35
+
36
+ return data.results.map((result: any) => {
37
+ let url = result.primaryPaperLink?.url;
38
+ if (!url && result.links && result.links.length > 0) {
39
+ url = result.links[0];
40
+ }
41
+ if (
42
+ !url &&
43
+ result.alternatePaperLinks &&
44
+ result.alternatePaperLinks.length > 0
45
+ ) {
46
+ url = result.alternatePaperLinks[0].url;
47
+ }
48
+ if (!url) {
49
+ url = `https://www.semanticscholar.org/paper/${result.id}`;
50
+ }
51
+
52
+ const title = result.title?.text || "";
53
+ const abstract = result.abstract?.text || "";
54
+
55
+ const metadata: string[] = [];
56
+ if (result.authors && result.authors.length > 0) {
57
+ const authorNames = result.authors
58
+ .map((a: any) => a.name)
59
+ .slice(0, 3);
60
+ metadata.push(
61
+ `Authors: ${authorNames.join(", ")}${
62
+ result.authors.length > 3 ? " et al." : ""
63
+ }`
64
+ );
65
+ }
66
+ if (result.year) metadata.push(`Year: ${result.year}`);
67
+ if (result.venue) metadata.push(`Venue: ${result.venue}`);
68
+ if (result.citationCount) metadata.push(`Citations: ${result.citationCount}`);
69
+
70
+ const content =
71
+ metadata.length > 0
72
+ ? `${metadata.join(" | ")}\n\n${abstract}`
73
+ : abstract;
74
+
75
+ return { url, title, content, engine: "semantic_scholar" };
76
+ });
77
+ };
@@ -0,0 +1,44 @@
1
+ import { EngineFunction } from "../../types/search-engine-interface.js";
2
+
3
+ export const wikidata: EngineFunction = async (
4
+ query: string,
5
+ _page: number | undefined
6
+ ) => {
7
+ const params = new URLSearchParams({
8
+ action: "wbsearchentities",
9
+ search: query,
10
+ language: "en",
11
+ format: "json",
12
+ limit: "20",
13
+ });
14
+
15
+ const response = await fetch(
16
+ `https://www.wikidata.org/w/api.php?${params}`,
17
+ {
18
+ headers: {
19
+ "User-Agent":
20
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
21
+ },
22
+ }
23
+ );
24
+
25
+ if (!response.ok) return [];
26
+
27
+ const data = await response.json();
28
+
29
+ if (!data || !data.search) return [];
30
+
31
+ return data.search.map((item: any) => {
32
+ const id = item.id;
33
+ const title = item.label || id;
34
+ const description = item.description || "No description available";
35
+ const url = item.url || `https://www.wikidata.org/wiki/${id}`;
36
+
37
+ return {
38
+ url,
39
+ title,
40
+ content: description,
41
+ engine: "wikidata",
42
+ };
43
+ });
44
+ };
@@ -0,0 +1,59 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction } from "../../types/search-engine-interface.js";
3
+
4
+ export const baidu: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ wd: query,
10
+ rn: "10",
11
+ pn: String(((page || 1) - 1) * 10),
12
+ });
13
+ const response = await fetch(`https://www.baidu.com/s?${params}`, {
14
+ headers: {
15
+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
16
+ Accept:
17
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
18
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8",
19
+ Cookie: "BAIDUID=1234567890:FG=1",
20
+ },
21
+ });
22
+ if (!response.ok) return [];
23
+ const htmlString = await response.text();
24
+ const { document } = parseHTML(htmlString);
25
+
26
+ return Array.from(
27
+ document.querySelectorAll(
28
+ "#content_left > div.result, div.c-container"
29
+ )
30
+ )
31
+ .map((element) => {
32
+ const titleLink = element.querySelector("h3 a, h3.c-title a");
33
+ const url = titleLink?.getAttribute("href") || "";
34
+ const title = titleLink?.textContent?.trim() || "";
35
+
36
+ if (!url || !title) {
37
+ return null;
38
+ }
39
+
40
+ const content =
41
+ element
42
+ .querySelector(
43
+ "div.c-abstract, div.c-span9, span.content-right_2s-H4"
44
+ )
45
+ ?.textContent?.trim() || "";
46
+ const timestamp =
47
+ element
48
+ .querySelector("span.c-color-gray2, span.newTimeFactor_2s-H4")
49
+ ?.textContent?.trim() || "";
50
+
51
+ return {
52
+ url,
53
+ title,
54
+ content: timestamp ? `${timestamp}\n${content}` : content,
55
+ engine: "baidu",
56
+ };
57
+ })
58
+ .filter((r) => r !== null && r.url && r.title);
59
+ };
@@ -0,0 +1,30 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction } from "../../types/search-engine-interface.js";
3
+
4
+ export const bing: EngineFunction = async (q: string, page?: number) => {
5
+ const params = new URLSearchParams({
6
+ q,
7
+ first: String(((page || 1) - 1) * 10 + 1),
8
+ });
9
+ const response = await fetch(`https://www.bing.com/search?${params}`, {
10
+ headers: {
11
+ "User-Agent":
12
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
13
+ "Accept-Language": "en-US,en;q=0.9",
14
+ Cookie: "CONSENT=YES+",
15
+ },
16
+ });
17
+ if (!response.ok) return [];
18
+ const text = await response.text();
19
+ const { document } = parseHTML(text);
20
+ return Array.from(document.querySelectorAll("li.b_algo"))
21
+ .map((el) => {
22
+ return {
23
+ url: el.querySelector("h2 a")?.getAttribute("href") || "",
24
+ title: el.querySelector("h2 a")?.textContent?.trim() || "",
25
+ content: el.querySelector(".b_caption p")?.textContent?.trim() || "",
26
+ engine: "bing",
27
+ };
28
+ })
29
+ .filter((r) => r.url && r.title);
30
+ };
@@ -0,0 +1,54 @@
1
+ import { parseHTML } from "linkedom";
2
+ import { EngineFunction } from "../../types/search-engine-interface.js";
3
+
4
+ export const brave: EngineFunction = async (
5
+ query: string,
6
+ page: number | undefined
7
+ ) => {
8
+ const params = new URLSearchParams({
9
+ q: query,
10
+ p: String(page || 1),
11
+ });
12
+ const response = await fetch(`https://search.brave.com/search?${params}`, {
13
+ headers: {
14
+ "User-Agent":
15
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
16
+ Accept:
17
+ "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
18
+ "Accept-Language": "en-US,en;q=0.9",
19
+ },
20
+ });
21
+ if (!response.ok) return [];
22
+ const htmlString = await response.text();
23
+ const { document } = parseHTML(htmlString);
24
+
25
+ const snippets = document.querySelectorAll(".snippet");
26
+ console.log(`Brave: Found ${snippets.length} snippets`);
27
+
28
+ const data = Array.from(snippets)
29
+ .map((element) => {
30
+ const link = element.querySelector("a");
31
+ const url = link?.getAttribute("href") || "";
32
+ const title =
33
+ element.querySelector(".search-snippet-title")?.textContent?.trim() || "";
34
+ const content =
35
+ element
36
+ .querySelector(".content")
37
+ ?.textContent?.trim() || "";
38
+
39
+ return {
40
+ url,
41
+ title,
42
+ content,
43
+ engine: "brave",
44
+ };
45
+ })
46
+ .filter((r) => r.url && r.title);
47
+
48
+ console.log(`Brave: Extracted ${data.length} valid results`);
49
+ if (data.length === 0 && htmlString.length > 0) {
50
+ console.log("Brave: HTML sample:", htmlString.substring(0, 1000));
51
+ }
52
+
53
+ return data;
54
+ };