心动模式
必须使用hbuilderx 3.4.8-alpha及以上
安装插件
由于ECharts官方没有对uni-app的支持,所以我们需要安装第三方插件来使用ECharts
下载插件并导入HBuilder
安装echarts
使用npm下载:
npm install echarts
代码实现
index.vue
详情参见代码注释
<template>
<!-- 使用组件 -->
<view><l-echart ref="chart"></l-echart></view>
</template>
<script>
import * as echarts from 'echarts' //导入包
export default {
data() {
return {
option: { //指定图表的配置项和数据
series: [{ //图表属性
type: 'graph', //图表类型
layout: 'force', //采用力引导布局
edgeLabel: {
show: false //不显示标签
},
force: {
repulsion: 150 //节点间的斥力,数越大则节点间距越大
},
data: [] //节点数据
}]
},
};
},
mounted() {
var width = uni.getWindowInfo().screenWidth; //获取屏幕宽度
var height = uni.getWindowInfo().screenHeight; //获取屏幕高度
this.$refs.chart.resize({ //设置画布大小
width: width,
height: height
});
this.$refs.chart.init(echarts, chart => { //初始化图表
chart.setOption(this.option); //初始化图表数据
chart.showLoading(); //加载数据时的加载动画,以免页面卡死
uni.request({ //向后端发起请求
url: 'http://localhost:8080/users/data', //请求地址
method: 'GET', //请求方法
timeout: 60000, //超时时间,单位为毫秒。默认值为 60000
dataType: 'json', //返回的数据为 JSON,返回后会对返回的数据进行一次 JSON.parse
success(res) { // 成功回调函数
var jsonObj = JSON.parse(JSON.stringify(res.data)); //解析得到的JSON数据
for (var i = 0; i < jsonObj.length; i++) { //定义头像大小
jsonObj[i].symbolSize = jsonObj[i].symbolSize * width * 0.25;
};
chart.hideLoading(); //停止加载动画
chart.setOption({ //更新数据
series: [{
force: {
repulsion: width
},
data: jsonObj
}]
});
},
fail(err) { // 失败回调函数
chart.hideLoading(); //停止加载动画
uni.showToast({ //提示加载失败
title: '请稍后重试!',
icon: 'error' //图标:错误
});
}
});
chart.on('click', function(params) { //绑定点击事件
uni.showModal({ //弹窗
title: '跳转至用户主页', //标题
content: 'id : ' + params.dataIndex, //弹窗内容
cancelText: '再想想', //取消按钮的文字,默认为"取消"
confirmText: '我来啦', //确定按钮的文字,默认为"确定"
success(res) { //弹窗成功的回调函数
if (res.confirm) { //点击确定
uni.navigateTo({ //跳转页面
url: '/pages/user/user?id=' + params
.dataIndex, //页面路径,参数
fail: function(res) { // 跳转失败
console.log('跳转失败')
}
});
}
}
})
});
});
},
}
</script>
<style>
</style>
当前效果如下:
模拟后端数据
-
新建SpringBoot项目(这里使用Idea)
-
在application.yml文件中配置端口号:
-
添加实体类UserData:
使用了lmbok的@Data注解,用于自动添加get,set方法
只需在pom.xml文件内添加其依赖即可
maven官方依赖地址:https://mvnrepository.com/artifact/org.projectlombok/lombok
@Data public class UserData { private String id; private String symbol; private Double symbolSize; public UserData(String id, String symbol, Double symbolSize){ this.id = id; this.symbol = symbol; this.symbolSize = symbolSize; } }
-
添加controller类的UserController文件:
这里直接在controller层写完全部逻辑
@RestController 声明为Controller且返回JSON格式数据
@Slf4j lombok下的日志工具
@RequestMapping(“/users”) 需要请求路径为 /users
这里使用了GSon工具来将String数据转换为JSON数据,由于SpringBoot支持了GSon,所以我们只需要在pom.xml文件中引入其依赖即可
maven官方依赖地址:https://mvnrepository.com/artifact/com.google.code.gson/gson
@RestController
@Slf4j
@RequestMapping("/users")
public class UserController {
@GetMapping("/data")
public UserData[] getData() {
log.info("接到请求");
// 模拟已经从数据库中获取到数据
UserData[] data = new UserData[6];
data[0] = new UserData("1", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 1.0);
data[1] = new UserData("2", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.8);
data[2] = new UserData("3", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.55);
data[3] = new UserData("4", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.46);
data[4] = new UserData("5", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.22);
data[5] = new UserData("6", "image://https://ts1.cn.mm.bing.net/th?id=OIP-C.8PA76m8zwAMnRUfipfWA1AHaNK&w=120&h=185&c=8&rs=1&qlt=90&o=6&dpr=1.3&pid=3.1&rm=2", 0.13);
log.info("数据已返回:"+ Arrays.toString(data));
return data;
}
}
效果图
后端接收到前端发起的请求并返回:
前端接收到后端返回的数据并成功展示:
点击头像可以跳转页面:
余弦相似度算法
这里采用的是 余弦相似度算法 ,直接用即可,无需理解
余弦相似度算法详解: https://blog.csdn.net/zz_dd_yy/article/details/51926305
-
pom.xml引入依赖
<!--结合操作工具包--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.5</version> </dependency> <!--bean实体注解工具包--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> <!--汉语言包,主要用于分词--> <dependency> <groupId>com.hankcs</groupId> <artifactId>hanlp</artifactId> <version>portable-1.8.4</version> </dependency>
-
Tokenizer 分词工具类
package com.example.commoms; import com.example.dao.Word; import com.hankcs.hanlp.HanLP; import com.hankcs.hanlp.seg.common.Term; import java.util.List; import java.util.stream.Collectors; public class Tokenizer { /** * 分词 */ public static List<Word> segment(String sentence) { //1、 采用HanLP中文自然语言处理中标准分词进行分词 List<Term> termList = HanLP.segment(sentence); //上面控制台打印信息就是这里输出的 System.out.println(termList.toString()); //2、重新封装到Word对象中(term.word代表分词后的词语,term.nature代表改词的词性) return termList.stream().map(term -> new Word(term.word, term.nature.toString())).collect(Collectors.toList()); } }
-
Word 封装分词结果
package com.example.dao; import lombok.Data; import java.util.Objects; /** * 封装分词结果 */ @Data public class Word implements Comparable { // 词名 private String name; // 词性 private String pos; // 权重,用于词向量分析 private Float weight; public Word(String name, String pos) { this.name = name; this.pos = pos; } @Override public int hashCode() { return Objects.hashCode(this.name); } @Override public boolean equals(Object obj) { if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } final Word other = (Word) obj; return Objects.equals(this.name, other.name); } @Override public String toString() { StringBuilder str = new StringBuilder(); if (name != null) { str.append(name); } if (pos != null) { str.append("/").append(pos); } return str.toString(); } @Override public int compareTo(Object o) { if (this == o) { return 0; } if (this.name == null) { return -1; } if (o == null) { return 1; } if (!(o instanceof Word)) { return 1; } String t = ((Word) o).getName(); if (t == null) { return 1; } return this.name.compareTo(t); } }
-
CosineSimilarity 相似率具体实现工具类
package com.example.commoms; import com.example.dao.Word; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.CollectionUtils; import java.math.BigDecimal; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * 判定方式:余弦相似度,通过计算两个向量的夹角余弦值来评估他们的相似度 余弦夹角原理: 向量a=(x1,y1),向量b=(x2,y2) similarity=a.b/|a|*|b| a.b=x1x2+y1y2 * |a|=根号[(x1)^2+(y1)^2],|b|=根号[(x2)^2+(y2)^2] */ public class CosineSimilarity { protected static final Logger LOGGER = LoggerFactory.getLogger(CosineSimilarity.class); /** * 1、计算两个字符串的相似度 */ public static double getSimilarity(String text1, String text2) { //如果wei空,或者字符长度为0,则代表完全相同 if (StringUtils.isBlank(text1) && StringUtils.isBlank(text2)) { return 1.0; } //如果一个为0或者空,一个不为,那说明完全不相似 if (StringUtils.isBlank(text1) || StringUtils.isBlank(text2)) { return 0.0; } //这个代表如果两个字符串相等那当然返回1了(这个我为了让它也分词计算一下,所以注释掉了) // if (text1.equalsIgnoreCase(text2)) { // return 1.0; // } //第一步:进行分词 List<Word> words1 = Tokenizer.segment(text1); List<Word> words2 = Tokenizer.segment(text2); return getSimilarity(words1, words2); } /** * 2、对于计算出的相似度保留小数点后六位 */ public static double getSimilarity(List<Word> words1, List<Word> words2) { double score = getSimilarityImpl(words1, words2); //(int) (score * 1000000 + 0.5)其实代表保留小数点后六位 ,因为1034234.213强制转换不就是1034234。对于强制转换添加0.5就等于四舍五入 score = (int) (score * 1000000 + 0.5) / (double) 1000000; return score; } /** * 文本相似度计算 判定方式:余弦相似度,通过计算两个向量的夹角余弦值来评估他们的相似度 余弦夹角原理: 向量a=(x1,y1),向量b=(x2,y2) similarity=a.b/|a|*|b| a.b=x1x2+y1y2 * |a|=根号[(x1)^2+(y1)^2],|b|=根号[(x2)^2+(y2)^2] */ public static double getSimilarityImpl(List<Word> words1, List<Word> words2) { // 向每一个Word对象的属性都注入weight(权重)属性值 taggingWeightByFrequency(words1, words2); //第二步:计算词频 //通过上一步让每个Word对象都有权重值,那么在封装到map中(key是词,value是该词出现的次数(即权重)) Map<String, Float> weightMap1 = getFastSearchMap(words1); Map<String, Float> weightMap2 = getFastSearchMap(words2); //将所有词都装入set容器中 Set<Word> words = new HashSet<>(); words.addAll(words1); words.addAll(words2); AtomicFloat ab = new AtomicFloat();// a.b AtomicFloat aa = new AtomicFloat();// |a|的平方 AtomicFloat bb = new AtomicFloat();// |b|的平方 // 第三步:写出词频向量,后进行计算 words.parallelStream().forEach(word -> { //看同一词在a、b两个集合出现的此次 Float x1 = weightMap1.get(word.getName()); Float x2 = weightMap2.get(word.getName()); if (x1 != null && x2 != null) { //x1x2 float oneOfTheDimension = x1 * x2; //+ ab.addAndGet(oneOfTheDimension); } if (x1 != null) { //(x1)^2 float oneOfTheDimension = x1 * x1; //+ aa.addAndGet(oneOfTheDimension); } if (x2 != null) { //(x2)^2 float oneOfTheDimension = x2 * x2; //+ bb.addAndGet(oneOfTheDimension); } }); //|a| 对aa开方 double aaa = Math.sqrt(aa.doubleValue()); //|b| 对bb开方 double bbb = Math.sqrt(bb.doubleValue()); //使用BigDecimal保证精确计算浮点数 //double aabb = aaa * bbb; BigDecimal aabb = BigDecimal.valueOf(aaa).multiply(BigDecimal.valueOf(bbb)); //similarity=a.b/|a|*|b| //divide参数说明:aabb被除数,9表示小数点后保留9位,最后一个表示用标准的四舍五入法 double cos = BigDecimal.valueOf(ab.get()).divide(aabb, 9, BigDecimal.ROUND_HALF_UP).doubleValue(); return cos; } /** * 向每一个Word对象的属性都注入weight(权重)属性值 */ protected static void taggingWeightByFrequency(List<Word> words1, List<Word> words2) { if (words1.get(0).getWeight() != null && words2.get(0).getWeight() != null) { return; } //词频统计(key是词,value是该词在这段句子中出现的次数) Map<String, AtomicInteger> frequency1 = getFrequency(words1); Map<String, AtomicInteger> frequency2 = getFrequency(words2); //如果是DEBUG模式输出词频统计信息 // if (LOGGER.isDebugEnabled()) { // LOGGER.debug("词频统计1:\n{}", getWordsFrequencyString(frequency1)); // LOGGER.debug("词频统计2:\n{}", getWordsFrequencyString(frequency2)); // } // 标注权重(该词出现的次数) words1.parallelStream().forEach(word -> word.setWeight(frequency1.get(word.getName()).floatValue())); words2.parallelStream().forEach(word -> word.setWeight(frequency2.get(word.getName()).floatValue())); } /** * 统计词频 * * @return 词频统计图 */ private static Map<String, AtomicInteger> getFrequency(List<Word> words) { Map<String, AtomicInteger> freq = new HashMap<>(); //这步很帅哦 words.forEach(i -> freq.computeIfAbsent(i.getName(), k -> new AtomicInteger()).incrementAndGet()); return freq; } /** * 输出:词频统计信息 */ private static String getWordsFrequencyString(Map<String, AtomicInteger> frequency) { StringBuilder str = new StringBuilder(); if (frequency != null && !frequency.isEmpty()) { AtomicInteger integer = new AtomicInteger(); frequency.entrySet().stream().sorted((a, b) -> b.getValue().get() - a.getValue().get()).forEach( i -> str.append("\t").append(integer.incrementAndGet()).append("、").append(i.getKey()).append("=") .append(i.getValue()).append("\n")); } str.setLength(str.length() - 1); return str.toString(); } /** * 构造权重快速搜索容器 */ protected static Map<String, Float> getFastSearchMap(List<Word> words) { if (CollectionUtils.isEmpty(words)) { return Collections.emptyMap(); } Map<String, Float> weightMap = new ConcurrentHashMap<>(words.size()); words.parallelStream().forEach(i -> { if (i.getWeight() != null) { weightMap.put(i.getName(), i.getWeight()); } else { LOGGER.error("no word weight info:" + i.getName()); } }); return weightMap; } }
-
AtomicFloat 原子类
package com.example.commoms; import java.util.concurrent.atomic.AtomicInteger; /** * jdk没有AtomicFloat,写一个 */ public class AtomicFloat extends Number { private AtomicInteger bits; public AtomicFloat() { this(0f); } public AtomicFloat(float initialValue) { bits = new AtomicInteger(Float.floatToIntBits(initialValue)); } //叠加 public final float addAndGet(float delta) { float expect; float update; do { expect = get(); update = expect + delta; } while (!this.compareAndSet(expect, update)); return update; } public final float getAndAdd(float delta) { float expect; float update; do { expect = get(); update = expect + delta; } while (!this.compareAndSet(expect, update)); return expect; } public final float getAndDecrement() { return getAndAdd(-1); } public final float decrementAndGet() { return addAndGet(-1); } public final float getAndIncrement() { return getAndAdd(1); } public final float incrementAndGet() { return addAndGet(1); } public final float getAndSet(float newValue) { float expect; do { expect = get(); } while (!this.compareAndSet(expect, newValue)); return expect; } public final boolean compareAndSet(float expect, float update) { return bits.compareAndSet(Float.floatToIntBits(expect), Float.floatToIntBits(update)); } public final void set(float newValue) { bits.set(Float.floatToIntBits(newValue)); } public final float get() { return Float.intBitsToFloat(bits.get()); } @Override public float floatValue() { return get(); } @Override public double doubleValue() { return (double) floatValue(); } @Override public int intValue() { return (int) get(); } @Override public long longValue() { return (long) get(); } @Override public String toString() { return Float.toString(get()); } }
-
可以在方法中调用
CosineSimilarity.getSimilarity("String1","String2");
来比较String1和String2的相似度,相识度值越大就越相似,完全相等为1。
心动算法
我们基于余弦相似度算法进行加权中和便可得到心动用户:
-
分别得到用户的标签,好友,队伍,点赞文章字符串列表
-
将得到的列表分别于当前用户的列表进行相似度比较
-
将得到的相似度值进行加权:
- 标签较为重要,故占 50%
- 其次为好友,占 20%
- 文章,占 20%
- 队伍, 占 10%
-
可设置头像大小:
s y m b o l S i z e = 标签相识度 ∗ 0.5 + 好友相似度 ∗ 0.2 + 文章点赞相似度 ∗ 0.2 + 队伍相似度 ∗ 0.1 symbolSize=标签相识度*0.5+好友相似度*0.2+文章点赞相似度*0.2+队伍相似度*0.1 symbolSize=标签相识度∗0.5+好友相似度∗0.2+文章点赞相似度∗0.2+队伍相似度∗0.1