boost库文档站内搜索引擎

项目源码

boost_searcher_project: 我的boost搜索引擎项目

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

1.介绍boost准标准库

2.项目实现效果

搜索引擎根据我们所给的关键字,搜出来的结果展示都是以,网页标题、网页内容摘要和跳转的网址组成的。但是它可能还有相应的照片、视频、广告,这些我们在设计基于Boost库的搜索引擎项目的时候,不考虑这些.

3.搜索引擎宏观架构图

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

  1. 我们要实现出boost库的站内搜索引擎,红色虚线框内就是我们要实现的内容,总的分为客户端和服务器,详细分析如下:我们从客户端想要获取到大学生的相关信息(呈现在网页上的样子就是:网页的标题+摘要+网址),首先我们构建的服务器就要有对应的数据存在,这些数据从何而来,我们可以进行全网的一个爬虫,将数据爬到我们的服务器的磁盘上,但是由于我的服务器性能原因,项目里没有实现.
  2. 现在数据已经被我们放到了磁盘中了,接下来客户端要访问服务器,那么服务器首先要运行起来,服务器一旦运行起来,它首先要做的工作就是对磁盘中的这些数据进行去标签和数据清洗的动作,因为我们从boost库拿的数据其实就是对应文档html网页,但是我们需要的只是每个网页的标题+网页内容摘要+跳转的网址,所以才有了去标签和数据清洗(只拿我们想要的)。这样就可以直接跳过网址跳转到boost库相应文档的位置。
  3. 服务器完成了去标签和数据清洗之后,就需要对这些清洗后的数据建立索引(方便客户端快速查找)
  4. 当服务器所以的工作都完成之后,客户端就发起http请求,通过GET方法,上传搜索关键,服务器收到了会进行解析,通过客户端发来的关键字去检索已经构建好的索引,找到了相关的html后,就会将逐个的将每个网页的标题、摘要和网址拼接起来,构建出一个新的网页,响应给客户端;至此,客户就看到了相应的内容,点击网址就可以跳转到boost库相应的文档位置。

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

模拟一次查找的过程:       
用户输入:小米 ---> 去倒排索引中查找关键字“小米” ---> 提取出文档ID【1,2】---> 去正排索引中,根据文档ID【1,2】找到文档内容 ---> 通过 [ 标题 + 内容摘要 + 网址 ] 的形式,构建响应结果 ---> 返回给用户小米相关的网页信息。

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

技术栈:C/C++ C++11 STL boost准标准库 JsonCPP cppjieba cpp-httplib 
html css js jQuery Ajax

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

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


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

Paser模块主逻辑 

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

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

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

    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

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

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

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分隔

//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建立索引模块

索引的相关结构

 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

     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;
        }

字符串切分

 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

  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

 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

Search

  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 指针指向的字符串中。

        // 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模块

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服务器的类。

五 前端模块

<!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请求。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值