基于boost准标准库的搜索引擎项目

零 项目背景/原理/技术栈

1.介绍boost准标准库

2.项目实现效果

3.搜索引擎宏观架构图

这是一个基于Web的搜索服务架构

  1. 客户端-服务器模型:采用了经典的客户端-服务器模型,用户通过客户端与服务器交互,有助于集中管理和分散计算。
  2. 简单的用户界面:客户端似乎很简洁,用户通过简单的HTTP请求与服务端交互,易于用户操作。
  3. 搜索引擎功能:服务器端的搜索器能够接收查询请求,从数据存储中检索信息,这是Web搜索服务的核心功能。
  4. 数据存储:有专门的存储系统用于存放数据文件(如HTML文件),有助于维护数据的完整性和持久性。
  5. 模块分离:搜索器、存储和处理请求的模块被分开,这有助于各模块独立更新和维护.

4.搜索过程的原理~正排,倒排索引

5.技术栈和项目环境,工具

技术栈: C/C++ C++11 STL boost准标准库 JsonCPP cppjieba cpp-httplib

html css js jQuery Ajax

**项目环境:**Centos7 华为云服务器 gcc/g++/makefile Vscode

一 Paser数据清洗,获取数据源模块

cpp 复制代码
const std::string src_path = "data/input/";
const std::string output_file = "data/output/dest.txt";
cpp 复制代码
class DocInfo
{
public:
    std::string _title;
    std::string _content;
    std::string _url;
};

Paser模块主逻辑

cpp 复制代码
int main()
{
    std::vector<std::string> files_list;
    // 第一步 把搜索范围src_path内的所有html的路径+文件名放到 files_list中
    if (!EnumFileName(src_path, &files_list))
    {
        lg(_Error,"%s","enum filename err!");
        exit(EnumFileNameErr);
    }

    // 第二步 将files_list中的文件打开,读取并解析为DocInfo后放到 web_documents中
    std::vector<DocInfo> html_documents;
    if (!ParseHtml(files_list, &html_documents))
    {
        lg(_Error,"%s","parse html err!");
        exit(ParseHtmlErr);
    }

    // 第三步 将web_documents的信息写入到 output_file文件中, 以\3为每个文档的分隔符
    if (!SaveHtml(html_documents, output_file))
    {
        lg(_Error,"%s","save html err!");
        exit(SaveHtmlErr);
    }
}
  1. 枚举文件 :从给定的源路径(src_path)中枚举所有HTML文件,并将它们的路径和文件名放入**files_list**中。

  2. 解析HTML :读取**files_list** 中的每个文件,解析它们为**DocInfo** 对象(可能包含标题、URL、正文等元素),然后存储到html_documents向量中。

  3. 保存文档 :将**html_documents** 中的文档信息写入到指定的输出文件**output_file** 中,文档之间用\3(ASCII码中的End-of-Text字符)分隔。

EnumFileName

cpp 复制代码
bool EnumFileName(const std::string &src_path, std::vector<std::string> *files_list)
{
    namespace fs = boost::filesystem;
    fs::path root_path(src_path);
    if (!fs::exists(root_path)) // 判断路径是否存在
    {
        lg(_Fatal,"%s%s",src_path.c_str()," is not exist");
        return false;
    }

    // 定义一个空迭代器,用来判断递归是否结束
    fs::recursive_directory_iterator end;
    // 递归式遍历文件
    for (fs::recursive_directory_iterator it(src_path); it != end; it++)
    {
        if (!fs::is_regular(*it))
            continue; // 保证是普通文件
        if (it->path().extension() != ".html")
            continue; // 保证是.html文件

        files_list->push_back(it->path().string()); // 插入的都是合法 路径+.html文件名
    }

    return true;
}

ParseHtml

cpp 复制代码
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo> *html_documents)
{
    for (const std::string &html_file_path : files_list)
    {
        // 第一步 遍历files_list,根据路径+文件名,读取html文件内容
        std::string html_file;
        if (!ns_util::FileUtil::ReadFile(html_file_path, &html_file))
        {
            lg(_Error,"%s","ReadFile err!");
            continue;
        }
        DocInfo doc_info;
        // 第二步 解析html文件,提取title
        if (!ParseTitle(html_file, &doc_info._title))
        {
            lg(_Error,"%s%s","ParseTitle err! ",html_file_path.c_str());
            continue;
        }
        // 第三步 解析html文件,提取content(去标签)
        if (!ParseContent(html_file, &doc_info._content))
        {
            lg(_Error,"%s","ParseContent err!");
            continue;
        }
        // 第四步 解析html文件,构建url
        if (!ParseUrl(html_file_path, &doc_info._url))
        {
            lg(_Error,"%s","ParseUrl err!");
            continue;
        }

        // 解析html文件完毕,结果都保存到了doc_info中
        // ShowDcoinfo(doc_info);
        html_documents->push_back(std::move(doc_info)); // 尾插会拷贝,效率不高,使用move
    }
    lg(_Info,"%s","ParseHtml success!");
    return true;
}

1.ReadFile

cpp 复制代码
    class FileUtil
    {
    public:
        static bool ReadFile(const std::string &file_path, std::string *out)
        {
            std::ifstream in(file_path, std::ios::in); // 以输入方式打开文件
            if (!in.is_open())
            {
                lg(_Fatal,"%s%s%s","ReadFile:",file_path.c_str()," open err!");
                return false;
            }

            std::string line;
            while (std::getline(in, line))
            {
                *out += line;
            }
            in.close();

            return true;
        }
    };

2.ParseTitle

cpp 复制代码
static bool ParseTitle(const std::string &html_file, std::string *title)
{
    size_t left = html_file.find("<title>");
    if (left == std::string::npos)
        return false;
    size_t right = html_file.find("</title>");
    if (right == std::string::npos)
        return false;

    int begin = left + std::string("<title>").size();
    int end = right;
    // 截取[begin,end-1]内的子串就是标题内容
    if (end-begin<0)
    {
        lg(_Error,"%s%s%s","ParseTitle:",output_file.c_str(),"has no title");
        return false;
    }

    std::string str = html_file.substr(begin, end - begin);
    *title = str;
    return true;
}

3.ParseContent

cpp 复制代码
static bool ParseContent(const std::string &html_file, std::string *content)
{
    // 利用简单状态机完成去标签工作
    enum Status
    {
        Lable,
        Content
    };

    Status status = Lable;
    for (char ch : html_file)
    {
        switch (status)
        {
        case Lable:
            if (ch == '>')
                status = Content;
            break;
        case Content:
            if (ch == '<')
                status = Lable;
            else
            {
                // 不保留html文本中自带的\n,防止后续发生冲突
                if (ch == '\n')
                    ch = ' ';
                content->push_back(ch);
            }
            break;
        default:
            break;
        }
    }

    return true;
}

4.ParseUrl

cpp 复制代码
static bool ParseUrl(const std::string &html_file_path, std::string *url)
{
    std::string url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";
    std::string url_tail = html_file_path.substr(src_path.size());

    *url = url_head + "/" + url_tail;
    return true;
}

SaveHtml

doc_info内部用\3分隔,doc_info之间用\n分隔

cpp 复制代码
//doc_info内部用\3分隔,doc_info之间用\n分隔
bool SaveHtml(const std::vector<DocInfo> &html_documents, const std::string &output_file)
{
    const char sep = '\3';
    std::ofstream out(output_file, std::ios::out | std::ios::binary|std::ios::trunc);
    if (!out.is_open())
    {
        lg(_Fatal,"%s%s%s","SaveHtml:",output_file.c_str()," open err!");
        return false;
    }

    for(auto &doc_info:html_documents)
    {
        std::string outstr;
        outstr += doc_info._title;
        outstr += sep;
        outstr += doc_info._content;
        outstr += sep;
        outstr+= doc_info._url;
        outstr+='\n';

        out.write(outstr.c_str(),outstr.size());
    }
    out.close();
    lg(_Info,"%s","SaveHtml success!");
    return true;
}

二 Index建立索引模块

索引的相关结构

cpp 复制代码
 class DocInfo // 解析后的html文档的相关信息
    {
    public:
        std::string _title;
        std::string _content;
        std::string _url;
        uint64_t _doc_id;
    };
    class InvertedElem
    {
    public:
        uint64_t _doc_id;
        std::string _word;
        int _weight; // 关键词word在该文档内的权重,方便后续查找时按顺序显示
    };

  1. 私有化构造函数和析构函数 :通过将构造函数和析构函数设为私有,禁止了外部通过常规方式创建Index类的实例。

  2. 禁用拷贝构造函数和拷贝赋值操作符 :通过将拷贝构造函数和赋值操作符标记为delete,防止了类的拷贝,确保了单例的唯一性。

  3. 静态实例和互斥锁 :用静态成员变量instance来存储这个类的唯一实例,并使用静态互斥锁_mutex来保证在多线程环境下的线程安全。

  4. GetInstance方法 :这是一个静态方法,用于获取Index类的唯一实例。如果instance为空,则实例化一个新的Index对象。这个方法在创建实例之前和之后都有一次判断实例是否为空的逻辑,这是"双重检查锁定"模式,它可以减少每次调用GetInstance方法时所需的锁定操作,从而提高性能。

  5. 正向索引和倒排索引的存储结构 :类中定义了两个私有成员变量来存储正向索引_forward_index和倒排索引_inverted_index。正向索引是一个vector,存储文档信息DocInfo对象,而倒排索引是一个unordered_map,它映射一个字符串(关键词)到一个InvertedListvector<InvertedElem>)。

  6. 构建索引的方法 :类提供了两个方法BuildForwardIndexBuildInvertedIndex,分别用于构建正向索引和倒排索引。这两个方法的具体实现在这个代码片段中没有给出。

  7. 检索功能的方法BuildIndex方法可能用于建立索引,GetForwardIndexGetInvertedList方法分别用于获取正向索引和倒排索引中的数据。

BuildIndex

cpp 复制代码
     bool BuildIndex(const std::string &input_path) // 构建索引
        {
            std::fstream in(input_path, std::ios::in | std::ios::binary);
            if (!in.is_open())
            {
                lg(_Fatal,"%s%s%s","BuildIndex fail! ",input_path.c_str()," cannot open");
                return false;
            }

            std::string html_line; // 每个html的的DocInfo以\n间隔
            int cnt=1; //debug
            while (std::getline(in, html_line))
            {
                DocInfo *doc_info = BuildForwardIndex(html_line);
                if (doc_info == nullptr)
                {
                    lg(_Error,"%s%s%s%s","BuildForwardIndex fail! ","who? ",html_line.c_str(),"  continue next html");
                    continue;
                }
                if (!BuildInvertedIndex(*doc_info))
                {
                    lg(_Error,"%s%s%d","BuildInvertedIndex fail! ","id: ",doc_info->_doc_id);
                    continue;
                }
                ++cnt;
                if(cnt%100 == 0)
                std::cout<<"cnt:"<<cnt<<std::endl; 
            }
            lg(_Info,"%s%d","BuildIndex over cnt:",cnt);
            in.close();
            return true;
        }

字符串切分

cpp 复制代码
 class StringUtil
    {
    public:
        static void SplitString(const std::string &str, std::vector<std::string> *ret_strs, const std::string &sep)
        {
            boost::split(*ret_strs, str, boost::is_any_of(sep), boost::token_compress_on);
        }
    };

    const char *const DICT_PATH = "./dict/jieba.dict.utf8";
    const char *const HMM_PATH = "./dict/hmm_model.utf8";
    const char *const USER_DICT_PATH = "./dict/user.dict.utf8";
    const char *const IDF_PATH = "./dict/idf.utf8";
    const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";
    class JiebaUtil
    {
    public:
        static void CutString(const std::string &src,std::vector<std::string> *ret)
        {
            _jieba.CutForSearch(src,*ret);
        }
    private:
        static cppjieba::Jieba _jieba;
    };
    cppjieba::Jieba JiebaUtil::_jieba(DICT_PATH,
                                      HMM_PATH,
                                      USER_DICT_PATH,
                                      IDF_PATH,
                                      STOP_WORD_PATH);

这段代码展示了两个C++工具类StringUtilJiebaUtil,它们都包含静态方法,用于处理字符串分割和中文分词功能。

  1. StringUtil

    • 这个类提供了一个静态方法SplitString,它使用Boost库的split函数来将字符串str依据分隔符sep分割,并将结果存储在传入的向量ret_strs中。
    • boost::token_compress_on参数指定如果分隔符在字符串中连续出现,那么多个分隔符将被视作一个。
  2. JiebaUtil

    • 这个类提供了一个静态方法CutString,它用于中文的分词。方法接受一个源字符串src和一个用于存储分词结果的向量ret
    • 类包含一个私有静态成员_jieba,它是cppjieba::Jieba类的一个实例。cppjieba::Jieba是一个中文分词库的C++实现。
    • 类在底部使用_jieba成员的静态初始化语法来初始化这个Jieba分词器实例。

常量路径定义: 代码中还定义了一些指向分词所需字典文件的路径常量:

  • DICT_PATH:指向基础字典文件。
  • HMM_PATH:指向用于HMM(隐马尔可夫模型)的模型文件。
  • USER_DICT_PATH:指向用户自定义的词典文件。
  • IDF_PATH:指向逆文档频率(IDF)字典文件。
  • STOP_WORD_PATH:指向停用词字典文件。

BuildForwardIndex

cpp 复制代码
  DocInfo *BuildForwardIndex(const std::string &html_line)
        {
            // 1~ 切分字符串
            std::vector<std::string> ret_strs;
            const std::string sep = "\3";
            ns_util::StringUtil::SplitString(html_line, &ret_strs, sep);
            if (ret_strs.size() < 3)
                return nullptr;

            // 2~ 填充doc_info
            DocInfo doc_info;
            doc_info._title = ret_strs[0];
            doc_info._content = ret_strs[1];
            doc_info._url = ret_strs[2];
            doc_info._doc_id = _forward_index.size(); // 插入第一个时id== size ==0

            // 3~ 插入到正排索引_forward_index
            _forward_index.push_back(std::move(doc_info));
            return &_forward_index.back();
        }

BuildInvertedIndex

cpp 复制代码
 bool BuildInvertedIndex(const DocInfo &doc_info)
        {
            struct words_cnt
            {
                int title_cnt = 0;
                int content_title = 0;
            };
            // 1~ 对doc_info的title和content进行分词
            std::unordered_map<std::string, words_cnt> words_frequency;
            std::vector<std::string> words_title;//保存title分词后的结果
            std::vector<std::string> words_content;//保存content分词后的结果
            ns_util::JiebaUtil::CutString(doc_info._title, &words_title);
            ns_util::JiebaUtil::CutString(doc_info._content, &words_content);
            // 2~ 统计词频填充words_frequency
            for (auto &word : words_title)//to_lower转换不能是const修饰
            {
                boost::to_lower(word); // 需要统一转化成为小写,因为搜索时不区分大小写
                //boost::to_lower_copy(word);
                words_frequency[word].title_cnt++;
            }
            for (auto &word : words_content)
            {
                boost::to_lower(word); // 需要统一转化成为小写,因为搜索时不区分大小写
                //boost::to_lower_copy(word);
                words_frequency[word].content_title++;
            }

            // 3~ 自定义权重 title:content = 10:1
            static const int title_weight = 10;
            static const int content_weight = 1;
            // 4~ 对words_frequency内的每个关键词创建InvertedElem并填充
            for (const auto &kv : words_frequency)
            {
                InvertedElem inverted_ele;
                inverted_ele._doc_id = doc_info._doc_id;
                inverted_ele._word = kv.first;
                inverted_ele._weight =
                    title_weight * kv.second.title_cnt +
                    content_weight * kv.second.content_title;

            // 5~ 将该文档的所有InvertedElem分别插入到倒排索引 _inverted_index中
            InvertedList &inverted_list = _inverted_index[kv.first];
            inverted_list.push_back(std::move(inverted_ele));
                //_inverted_index[kv.first].push_back(std::move(inverted_ele));
            }

            return true;
        }

三 Searcher搜索模块

InitSearcher

  1. 分词处理 : 用户输入的查询字符串 query 通过 ns_util::JiebaUtil::CutString 函数进行分词,分词结果存储在 key_words 向量中。

  2. 搜索和去重 : 遍历分词后的关键词。对每个关键词,都先将其转换为小写以实现大小写不敏感的搜索,然后获取对应的倒排索引链(InvertedList)。如果倒排索引链存在,遍历链中的每个元素,并在 tokens_map 中以文档ID为键聚合数据,合并权重和关键词,实现对同一文档的去重。

  3. 排序 : 将 tokens_map 中聚合的结果转移到一个向量 inverted_ele_all 中,并根据权重对其进行降序排序,这样权重高的(更相关的)文档会排在前面。

  4. 构建JSON结果 : 遍历排序后的 inverted_ele_all 向量,对于每个元素,使用它的文档ID去查询正向索引获取文档的详细信息,如标题、内容和URL。将这些信息构建成一个JSON对象,并添加到一个 Json::Value 类型的 ret 数组中。函数最后使用 Json::FastWriterret 转换成JSON格式的字符串并存储在 json_str 指针指向的字符串中。

cpp 复制代码
        // query是用户输入的搜索关键字
        // json_str是返回给用户浏览器的搜索结果
        void Search(const std::string &query, std::string *json_str)
        {
            // 1~对query进行分词
            std::vector<std::string> key_words;
            ns_util::JiebaUtil::CutString(query, &key_words);
            std::unordered_map<uint64_t,InvertedElemDedup> tokens_map;//去重id后的结果

            for (auto &key_word : key_words)
            {
                // 查询的关键词全部转换为小写,提取出来的信息不区分大小写
                boost::to_lower(key_word);
                // 2~对分词结果 分别进行搜索
                ns_index::Index::InvertedList *inverted_list =
                    _index->GetInvertedList(key_word);
                if (inverted_list == nullptr)
                {
                    continue; // 这个词没能找到 对应的倒排拉链
                }
                for(auto &elem: *inverted_list)
                {
                    auto& dedup_ele = tokens_map[elem._doc_id];
                    dedup_ele._doc_id = elem._doc_id;
                    dedup_ele._weight += elem._weight;
                    dedup_ele._words.push_back(elem._word);
                }
            }
            // 优化点:对所有的ele合并后指向的doc_id进行去重 这里只关心weight和id
            std::vector<InvertedElemDedup> inverted_ele_all;
            for(auto &kv:tokens_map)
            {
                inverted_ele_all.push_back(std::move(kv.second));
            }
            // 3~对所有的inverted_element按照wegiht排序
            sort(inverted_ele_all.begin(), inverted_ele_all.end(),[](InvertedElemDedup& left,InvertedElemDedup& right){
                return left._weight > right._weight;
            });
            // 4~序列化,构建json串返回给用户 -- 使用jsoncpp
            Json::Value ret;
            int cnt = 0; // debug
            for (auto &ele : inverted_ele_all)
            {
                ns_index::DocInfo *doc_info = _index->GetForwardIndex(ele._doc_id);
                if (doc_info == nullptr)
                    continue;
                Json::Value element;
                element["title"] = doc_info->_title;
                // 搜索时需要摘要,不是所有的content,后面优化
                element["desc"] = GetDesc(doc_info->_content, ele._words[0]);
                element["url"] = doc_info->_url;
                // element["weight"] = ele._weight;
                // element["word"] = ele._words[0];
                // element["id"] = (int)ele._doc_id; // json自动将int转化为string
                ret.append(element);
            }
            //Json::StyledWriter writer;
            Json::FastWriter writer;
            *json_str = writer.write(ret);
        }

    private:
        std::string GetDesc(const std::string &html_content, const std::string &word)
        {
            // 找到word在content中首次出现的位置,向前截取prev_stepbyte,向后截取next_stepbyte
            // 向前<prev_step则从content开头开始,向后不足next_step则到content结尾
            // 1~ 找到word首次出现的位置
            std::cout << word << std::endl; // debug
            auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(),
                                    [](int l, int r)
                                    {
                                        return std::tolower(l) == std::tolower(r);
                                    });
            if (iter == html_content.end())
            {
                lg(_Error,"%s","content里面没找到word");
                return "None1";
            }
            // 找到了
            int pos = std::distance(iter, html_content.end());
            const int prev_step = 50;
            const int next_step = 50;
            // 2~ 确定begin和end位置

            int begin = pos >= prev_step ? pos - prev_step : 0;
            int end = (pos + next_step) < html_content.size() ? pos + next_step : html_content.size();

            // 3~ 截取描述子串[begin,end)并返回
            if (begin >= end) // end一定大于begin
            {
                lg(_Error,"%s","begin > end 越界了");
                return "None2";
            }
            std::string desc = html_content.substr(begin, end - begin);
            desc += "...";
            return desc;
        }
    };

四 http_server模块

cpp 复制代码
const std::string input = "data/output/dest.txt";//从input里读取数据构建索引
const std::string root_path = "./wwwroot";
int main()
{
        std::unique_ptr<ns_searcher::Searcher> searcher(new ns_searcher::Searcher());
        searcher->SearcherInit(input);
        httplib::Server svr;
        svr.set_base_dir(root_path.c_str()); // 设置根目录
        // 重定向到首页
        svr.Get("/", [](const httplib::Request &, httplib::Response &rsp)
                { rsp.set_redirect("/home/LZF/boost_searcher_project/wwwroot/index.html"); });
        svr.Get("/s",[&searcher](const httplib::Request &req,httplib::Response &rsp)
        {
                if(!req.has_param("word"))
                {
                        rsp.set_content("无搜索关键字!","test/plain,charset=utf-8");
                        return;
                }
                std::string json_str;
                std::string query = req.get_param_value("word");
                std::cout<<"用户正在搜索: "<<query<<std::endl;
                searcher->Search(query,&json_str);
                rsp.set_content(json_str,"application/json");
        });

        svr.listen("0.0.0.0", 8800);
}
  1. 初始化 : 定义了 inputroot_path 两个字符串常量,分别表示索引文件的路径和服务器的根目录。

  2. 创建搜索对象 : 使用 std::unique_ptr 创建了 Searcher 类的一个实例,并通过 SearcherInit 方法初始化,以从指定的 input 文件中构建索引。

  3. 创建和配置服务器 : 使用 httplib::Server 类创建了一个HTTP服务器实例,设置了服务器的根目录为 root_path

  4. 首页重定向 : 服务器对根路径 / 的GET请求进行处理,通过 set_redirect 方法将请求重定向到指定的HTML页面路径。

  5. 搜索请求处理 : 对路径 /s 的GET请求进行处理,这是搜索功能的实现部分。服务器检查请求中是否包含名为 word 的参数:

    • 如果请求中没有 word 参数,则返回错误信息。
    • 如果有,它将提取 word 参数的值,打印出查询的内容,并调用 Searcher 实例的 Search 方法来进行搜索。搜索的结果是一个JSON字符串,它会设置为响应体的内容。
  6. 启动服务器 : 使用 svr.listen 方法监听 0.0.0.0 上的 8800 端口,使服务器开始接受连接和处理请求。

httplib::Serverhttplib 库中用于创建和管理HTTP服务器的类。

五 前端模块

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>

    <title>boost 搜索引擎</title>
    <style>
        /* 去掉网页中的所有的默认内外边距,html的盒子模型 */
        * {
            /* 设置外边距 */
            margin: 0;
            /* 设置内边距 */
            padding: 0;
        }
        /* 将我们的body内的内容100%和html的呈现吻合 */
        html,
        body {
            height: 100%;
        }
        /* 类选择器.container */
        .container {
            /* 设置div的宽度 */
            width: 800px;
            /* 通过设置外边距达到居中对齐的目的 */
            margin: 0px auto;
            /* 设置外边距的上边距,保持元素和网页的上部距离 */
            margin-top: 15px;
        }
        /* 复合选择器,选中container 下的 search */
        .container .search {
            /* 宽度与父标签保持一致 */
            width: 100%;
            /* 高度设置为52px */
            height: 52px;
        }
        /* 先选中input标签, 直接设置标签的属性,先要选中, input:标签选择器*/
        /* input在进行高度设置的时候,没有考虑边框的问题 */
        .container .search input {
            /* 设置left浮动 */
            float: left;
            width: 600px;
            height: 50px;
            /* 设置边框属性:边框的宽度,样式,颜色 */
            border: 1px solid black;
            /* 去掉input输入框的有边框 */
            border-right: none;
            /* 设置内边距,默认文字不要和左侧边框紧挨着 */
            padding-left: 10px;
            /* 设置input内部的字体的颜色和样式 */
            color: #CCC;
            font-size: 14px;
        }
        /* 先选中button标签, 直接设置标签的属性,先要选中, button:标签选择器*/
        .container .search button {
            /* 设置left浮动 */
            float: left;
            width: 150px;
            height: 52px;
            /* 设置button的背景颜色,#4e6ef2 */
            background-color: #4e6ef2;
            /* 设置button中的字体颜色 */
            color: #FFF;
            /* 设置字体的大小 */
            font-size: 19px;
            font-family:Georgia, 'Times New Roman', Times, serif;
        }
        .container .result {
            width: 100%;
        }
        .container .result .item {
            margin-top: 15px;
        }

        .container .result .item a {
            /* 设置为块级元素,单独站一行 */
            display: block;
            /* a标签的下划线去掉 */
            text-decoration: none;
            /* 设置a标签中的文字的字体大小 */
            font-size: 20px;
            /* 设置字体的颜色 */
            color: #4e6ef2;
        }
        .container .result .item a:hover {
            text-decoration: underline;
        }
        .container .result .item p {
            margin-top: 5px;
            font-size: 16px;
            font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
        }

        .container .result .item i{
            /* 设置为块级元素,单独站一行 */
            display: block;
            /* 取消斜体风格 */
            font-style: normal;
            color: green;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="search">
            <input type="text" value="请输入搜索关键字">
            <button onclick="Search()">搜索一下</button>
        </div>
        <div class="result">
        </div>
    </div>
    <script>
        function Search(){
            let query = $(".container .search input").val();
            console.log("query = " + query);
    
            $.get("/s", {word: query}, function(data){
                console.log(data);
                BuildHtml(data);
            });
        }
    
        function BuildHtml(data){
            let result_lable = $(".container .result");
            result_lable.empty();
    
            for( let elem of data){
                let a_lable = $("<a>", {
                    text: elem.title,
                    href: elem.url,
                    target: "_blank"
                });
                let p_lable = $("<p>", {
                    text: elem.desc
                });
                let i_lable = $("<i>", {
                    text: elem.url
                });
                let div_lable = $("<div>", {
                    class: "item"
                });
                a_lable.appendTo(div_lable);
                p_lable.appendTo(div_lable);
                i_lable.appendTo(div_lable);
                div_lable.appendTo(result_lable);
            }
        }
    </script>
    
 
</body>
</html>

HTML结构

  1. 搜索栏 (div.search):

    • 包含一个文本输入框,用户可以在其中输入搜索关键字。
    • 包含一个按钮,当点击时会调用 Search() JavaScript函数。
  2. 搜索结果显示区域 (div.result):

    • 这是一个空的div,将来用来动态显示搜索结果。

样式

  • 通过CSS设置了页面和元素的样式,包括输入框、按钮、搜索结果等。

JavaScript功能

  1. Search() 函数:

    • 从输入框中获取用户输入的查询词。
    • 使用 jQuery$.get() 函数异步向服务器的 /s 路径发送一个GET请求,并将用户的查询词作为参数传递。
    • 当收到响应时,调用 BuildHtml() 函数处理数据并构建结果HTML。
  2. BuildHtml() 函数:

    • 清空结果显示区域,为新的搜索结果做准备。
    • 遍历响应数据中的每个搜索结果,并为每个结果创建包含标题、描述和URL的HTML元素。
    • 将创建的HTML元素附加到结果显示区域。

用户交互

  • 当用户输入查询词并点击搜索按钮时,页面将不会进行重新加载,而是通过JavaScript异步请求后端服务,并将结果动态地插入到页面中。

jQuery

  • 页面通过CDN引用了 jQuery 库,以简化DOM操作和Ajax请求。
相关推荐
康熙38bdc3 分钟前
Linux 环境变量
linux·运维·服务器
鄃鳕8 分钟前
HTTP【网络】
网络·网络协议·http
hakesashou1 小时前
python如何比较字符串
linux·开发语言·python
Ljubim.te1 小时前
Linux基于CentOS学习【进程状态】【进程优先级】【调度与切换】【进程挂起】【进程饥饿】
linux·学习·centos
cooldream20091 小时前
Linux性能调优技巧
linux
读心悦2 小时前
如何在 Axios 中封装事件中心EventEmitter
javascript·http
QMCY_jason2 小时前
Ubuntu 安装RUST
linux·ubuntu·rust
慕雪华年2 小时前
【WSL】wsl中ubuntu无法通过useradd添加用户
linux·ubuntu·elasticsearch
苦逼IT运维2 小时前
YUM 源与 APT 源的详解及使用指南
linux·运维·ubuntu·centos·devops
CXDNW2 小时前
【网络篇】计算机网络——应用层详述(笔记)
服务器·笔记·计算机网络·http·web·cdn·dns